/**
 * Servicio de autenticación con Keycloak
 *
 * Seguridad de tokens:
 *  - access_token  → en memoria (authStore) + cookies no-HttpOnly (una o varias si el JWT es grande)
 *  - refresh_token → cookie HttpOnly únicamente (JS nunca lo lee directamente)
 *  - El refresh se hace server-side via /api-sveltekit/auth/silent-refresh
 *  - NO se usa localStorage para tokens
 */

import Keycloak from 'keycloak-js';
import { writable, derived, get } from 'svelte/store';
import { browser } from '$app/environment';
import {
	clearAccessTokenOnDocument,
	getAccessTokenFromDocument,
	setAccessTokenOnDocument
} from '$lib/access-token-cookie-browser';

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

export interface User {
	id: string;
	username: string;
	email?: string;
	name?: string;
	firstName?: string | null;
	lastName?: string | null;
	displayName?: string | null;
	avatarUrl?: string | null;
	workspaceAvatarUrl?: string | null;
	legacyAvatarUrl?: string | null;
	tenantId?: number;
	roles: string[];
	permissions: string[];
	allowedSystems: string[]; // sistemas a los que tiene acceso: "fixed_asset" | "inventory"
	// Cache management
	profileSyncedAt?: number; // timestamp en ms para cache TTL
}

export interface AuthState {
	isAuthenticated: boolean;
	isLoading: boolean;
	user: User | null;
	token: string | null;
}

// ─────────────────────────────────────────────────────────
// Configuración de Keycloak
// ─────────────────────────────────────────────────────────

/**
 * Devuelve la URL pública de Keycloak correcta para el browser.
 * Si VITE_KEYCLOAK_URL está bakeado con localhost/127.0.0.1 pero el browser
 * no está en localhost (producción), se ignora el valor bakeado y se deriva
 * del hostname real del browser. Protege contra builds con .env de dev en prod.
 */
function resolveKeycloakUrl(): string {
	const configured = (import.meta.env.VITE_KEYCLOAK_URL || '').replace(/\/+$/, '');

	if (typeof window === 'undefined') {
		// SSR: usar el valor configurado tal cual (el server tiene las vars correctas)
		return configured || 'http://localhost:8085/kcauth';
	}

	const browserHostname = window.location.hostname;
	const isLocalBrowser = browserHostname === 'localhost' || browserHostname === '127.0.0.1';

	if (configured) {
		try {
			const parsed = new URL(configured);
			const configuredHost = parsed.hostname;
			const isLocalConfigured = configuredHost === 'localhost' || configuredHost === '127.0.0.1';
			// Si el build fue con localhost pero el browser NO está en localhost → derivar del hostname real
			if (isLocalConfigured && !isLocalBrowser) {
				const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
				return `${protocol}//${browserHostname}/kcauth`;
			}
		} catch {
			// URL malformada — caer al fallback
		}
		return configured;
	}

	if (isLocalBrowser) {
		return 'http://localhost:8085/kcauth';
	}

	const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
	return `${protocol}//${browserHostname}/kcauth`;
}

const keycloakConfig = {
	url: resolveKeycloakUrl(),
	realm: import.meta.env.VITE_KEYCLOAK_REALM,
	clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID
};

let keycloakInstance: Keycloak | null = null;
const AUTH_USER_SESSION_KEY = 'app_auth_user_v1';

function pickAvatar(...candidates: Array<unknown>): string | null {
	for (const candidate of candidates) {
		if (typeof candidate === 'string' && candidate.trim().length > 0) {
			return candidate.trim();
		}
	}
	return null;
}

function pickText(...candidates: Array<unknown>): string | null {
	for (const candidate of candidates) {
		if (typeof candidate === 'string') {
			const value = candidate.trim();
			if (value.length > 0) {
				return value;
			}
		}
	}
	return null;
}

function readUserFromSession(): User | null {
	if (!browser) return null;
	try {
		const raw = sessionStorage.getItem(AUTH_USER_SESSION_KEY);
		if (!raw) return null;
		const parsed = JSON.parse(raw) as User;
		if (!parsed || typeof parsed !== 'object') return null;
		if (!parsed.id || !parsed.username) return null;
		return parsed;
	} catch {
		return null;
	}
}

function persistUserInSession(user: User | null): void {
	if (!browser) return;
	if (!user) {
		sessionStorage.removeItem(AUTH_USER_SESSION_KEY);
		return;
	}
	sessionStorage.setItem(AUTH_USER_SESSION_KEY, JSON.stringify(user));
}

// ─────────────────────────────────────────────────────────
// Auth store (tokens solo en memoria)
// ─────────────────────────────────────────────────────────

const createAuthStore = () => {
	const { subscribe, set, update } = writable<AuthState>({
		isAuthenticated: false,
		isLoading: true,
		user: null,
		token: null
	});

	return {
		subscribe,
		setAuthenticated: (authenticated: boolean) =>
			update((s) => ({ ...s, isAuthenticated: authenticated })),
		setLoading: (loading: boolean) =>
			update((s) => ({ ...s, isLoading: loading })),
		setUser: (user: User | null) => {
			if (user === null) {
				persistUserInSession(null);
				update((s) => ({ ...s, user: null }));
				return;
			}
			update((s) => {
				const prev = s.user;
				const permissions = preserveNonEmptyArray(user.permissions, prev?.permissions);
				const roles = preserveNonEmptyArray(user.roles, prev?.roles);
				const allowedSystems = preserveNonEmptyArray(user.allowedSystems, prev?.allowedSystems);

				if (
					prev &&
					Array.isArray(user.permissions) &&
					user.permissions.length === 0 &&
					(prev.permissions?.length ?? 0) > 0
				) {
					console.debug(
						'[auth] setUser: previene downgrade de permissions',
						prev.permissions.length,
						'→ 0'
					);
				}

				const merged: User = { ...user, permissions, roles, allowedSystems };
				persistUserInSession(merged);
				return { ...s, user: merged };
			});
		},
		/** Asignación directa sin guard. Usar solo cuando el backend confirma el estado (p. ej. syncCompanyPermissions). */
		setUserUnsafe: (user: User | null) => {
			persistUserInSession(user);
			update((s) => ({ ...s, user }));
		},
		/** Limpia solo el usuario del store (logout parcial). Para logout completo usar reset(). */
		clearUser: () => {
			persistUserInSession(null);
			update((s) => ({ ...s, user: null }));
		},
		setToken: (token: string | null) => update((s) => ({ ...s, token })),
		/** ⚠️  Los tokens ya NO se guardan en localStorage; solo en memoria. */
		setTokens: (accessToken: string, _refreshToken?: string) => {
			update((s) => ({ ...s, token: accessToken }));
			// El refresh_token llega en cookie HttpOnly desde el servidor;
			// el cliente no lo almacena ni lo lee en ningún momento.
		},
		reset: () =>
			{
				persistUserInSession(null);
				set({
					isAuthenticated: false,
					isLoading: false,
					user: null,
					token: null
				});
			}
	};
};

export const authStore = createAuthStore();

export const isAuthenticated = derived(authStore, ($a) => $a.isAuthenticated);
export const currentUser = derived(authStore, ($a) => $a.user);

/**
 * Indica si los permisos RBAC del usuario para la compañía activa ya se hidrataron
 * en cliente (vía {@link syncCompanyPermissions} o tras la inicialización del
 * dashboard si no hay compañía). Las pantallas que muestran 403 según permisos
 * deben esperar a que esto sea `true` antes de decidir, para evitar el flash
 * de "Acceso restringido" en el primer render.
 */
export const permissionsHydrated = writable<boolean>(false);

/** true mientras `refreshPermissions` revalida permisos en segundo plano (sidebar). */
export const permissionsRefreshing = writable<boolean>(false);

export function markPermissionsHydrated(): void {
	permissionsHydrated.set(true);
}

/** No sobrescribir permisos RBAC con [] de /v1/auth/me (Hub no es fuente de verdad).
 *  Defensa en profundidad: el guard de authStore.setUser ya cubre esto. */
function mergePermissionsFromHub(incoming: unknown, previous: string[] | undefined): string[] {
	if (Array.isArray(incoming) && incoming.length > 0) return incoming;
	if (previous && previous.length > 0) return previous;
	return Array.isArray(incoming) ? (incoming as string[]) : [];
}

/** Preserva roles previos (p. ej. admin del Hub) al fusionar con /v1/auth/me.
 *  Defensa en profundidad: el guard de authStore.setUser ya cubre esto. */
function mergeRolesFromHub(incoming: unknown, previous: string[] | undefined): string[] {
	const prev = previous ?? [];
	if (Array.isArray(incoming) && incoming.length > 0) {
		return Array.from(new Set([...prev, ...incoming]));
	}
	return prev;
}

/**
 * Merge defensivo: preserva el array previo si el incoming es undefined,
 * null o [] (cuando previo no era vacío). Un incoming con items siempre
 * se respeta — la revocación parcial sí es válida.
 *
 * Para una revocación TOTAL legítima usa `authStore.setUserUnsafe()`.
 */
export function preserveNonEmptyArray<T>(
	incoming: T[] | null | undefined,
	previous: T[] | undefined
): T[] {
	const prev = previous ?? [];
	if (incoming == null) return prev;
	if (Array.isArray(incoming) && incoming.length === 0 && prev.length > 0) {
		return prev;
	}
	return Array.isArray(incoming) ? incoming : prev;
}

// ─────────────────────────────────────────────────────────
// Cache local de permisos del sidebar (localStorage, TTL 5 min)
// ─────────────────────────────────────────────────────────

const SIDEBAR_PERMS_CACHE_PREFIX = 'a76:perms:v1:';
const SIDEBAR_PERMS_CACHE_TTL_MS = 5 * 60 * 1000;

interface SidebarPermsCacheEntry {
	permissions: string[];
	roles: string[];
	allowedSystems: string[];
	tenantId?: number;
	cachedAt: number;
}

function buildSidebarPermsCacheKey(userId: string, companyId: number): string {
	return `${SIDEBAR_PERMS_CACHE_PREFIX}u${userId}:c${companyId}`;
}

function saveSidebarPermsCache(
	userId: string,
	companyId: number,
	data: Omit<SidebarPermsCacheEntry, 'cachedAt'>
): void {
	if (!browser) return;
	try {
		const entry: SidebarPermsCacheEntry = { ...data, cachedAt: Date.now() };
		localStorage.setItem(buildSidebarPermsCacheKey(userId, companyId), JSON.stringify(entry));
	} catch {
		// localStorage puede estar restringido — no romper la app
	}
}

export function loadSidebarPermsCache(
	userId: string,
	companyId: number
): SidebarPermsCacheEntry | null {
	if (!browser) return null;
	try {
		const raw = localStorage.getItem(buildSidebarPermsCacheKey(userId, companyId));
		if (!raw) return null;
		const entry = JSON.parse(raw) as SidebarPermsCacheEntry;
		if (!entry || typeof entry !== 'object') return null;
		if (!Array.isArray(entry.permissions) || !Array.isArray(entry.roles)) return null;
		if (Date.now() - (entry.cachedAt ?? 0) > SIDEBAR_PERMS_CACHE_TTL_MS) return null;
		return entry;
	} catch {
		return null;
	}
}

function clearSidebarPermsCache(userId?: string): void {
	if (!browser) return;
	try {
		const keys: string[] = [];
		for (let i = 0; i < localStorage.length; i++) {
			const key = localStorage.key(i);
			if (!key || !key.startsWith(SIDEBAR_PERMS_CACHE_PREFIX)) continue;
			if (userId === undefined || key.includes(`u${userId}:`)) {
				keys.push(key);
			}
		}
		for (const key of keys) {
			localStorage.removeItem(key);
		}
	} catch {
		// silencioso
	}
}

/**
 * Códigos de rol que dan bypass total a checks de permiso:
 * - `super_admin`: rol local de la compañía (fuente de verdad post-desacoplamiento).
 * - `admin`: rol del realm Keycloak (preservado por compat con `/v1/auth/me` del Hub).
 * - `hub_admin`: super-admin atestado por el Hub.
 */
const ADMIN_ROLE_CODES: ReadonlySet<string> = new Set(['super_admin', 'admin', 'hub_admin']);

export function userIsAdmin(user: User | null): boolean {
	if (!user) return false;
	return user.roles.some((role) => ADMIN_ROLE_CODES.has(role));
}

/**
 * Verifica si el usuario tiene un permiso específico.
 * `user.permissions` debe incluir códigos de la app (p. ej. `user.view`); se
 * cargan vía {@link syncCompanyPermissions} desde `/v1/core/permissions/me`.
 *
 * Bypass para super-admins: los códigos en {@link ADMIN_ROLE_CODES} dan acceso
 * total sin requerir un permiso granular específico.
 */
export function userHasPermission(user: User | null, permission: string): boolean {
	if (!user) return false;
	return userIsAdmin(user) || user.permissions.includes(permission);
}

// ─────────────────────────────────────────────────────────
// Inicialización
// ─────────────────────────────────────────────────────────

/**
 * Inicializa el estado de autenticación en el cliente.
 * - Si hay un access_token en la cookie no-HttpOnly, lo usa.
 * - En cualquier caso intenta inicializar Keycloak JS (para el flujo SSO).
 */
export const initAuth = async (): Promise<boolean> => {
	if (!browser) return false;

	try {
		authStore.setLoading(true);

		const sessionUser = readUserFromSession();
		if (sessionUser) {
			authStore.setUser(sessionUser);
		}

		// Restaurar token desde cookie no-HttpOnly (password login flow)
		const cookieToken = getAccessTokenFromDocument();
		if (cookieToken) {
			authStore.setToken(cookieToken);
			authStore.setAuthenticated(true);
			await loadUserInfo(cookieToken).catch(() => { });
			authStore.setLoading(false);
			return true;
		}

		// Sin token local: no hay sesión en el cliente. El login entra SIEMPRE por
		// el App Launcher del Workspace (relay → /auth/sso → Hub /sso-exchange);
		// el CRM NO inicializa Keycloak en el browser. Si no hay token, el layout
		// del servidor reenvía al Workspace.
		authStore.setLoading(false);
		return false;
	} catch (err) {
		console.error('[auth] Error en initAuth:', err);
		authStore.setLoading(false);
		return false;
	}
};

/** Inicializa Keycloak JS para el flujo SSO con PKCE */
export const initKeycloak = async (): Promise<boolean> => {
	if (!browser) return false;

	try {
		keycloakInstance = new Keycloak(keycloakConfig);

		const authenticated = await keycloakInstance.init({
			onLoad: 'check-sso',
			silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
			pkceMethod: 'S256',
			checkLoginIframe: false
		});

		if (authenticated) {
			await updateAuthState();
			setupKeycloakTokenHooks();
		}

		return authenticated;
	} catch (err) {
		console.error('[auth] Error inicializando Keycloak:', err);
		return false;
	}
};

let previousTenantId: number | undefined = undefined;

const updateAuthState = async () => {
	if (!keycloakInstance?.authenticated) {
		authStore.reset();
		persistUserInSession(null);
		return;
	}

	try {
		const profile = await keycloakInstance.loadUserProfile();
		const token = keycloakInstance.token ?? null;
		const parsed = keycloakInstance.tokenParsed as any;

		// Roles y tenant_id se obtienen desde el backend (/v1/auth/me y /permissions/me),
		// no desde claims del JWT de Keycloak.
		const previousUser = get(authStore).user;
		const tenantId: number | undefined = previousUser?.tenantId;
		const roles: string[] = previousUser?.roles ?? [];

		const tenantChanged = previousTenantId !== undefined && previousTenantId !== tenantId;

		// Obtener permisos actuales para evitar que el SSO los borre si fallara el fetch posterior
		let currentPerms: string[] = [];
		try {
			const { get } = await import('svelte/store');
			const currentState = get(authStore);
			currentPerms = currentState.user?.permissions || [];
		} catch { }

		const firstName = pickText(profile.firstName, parsed?.given_name, previousUser?.firstName);
		const lastName = pickText(profile.lastName, parsed?.family_name, previousUser?.lastName);
		const fullNameFromParts = pickText(
			firstName && lastName ? `${firstName} ${lastName}` : null,
			firstName,
			lastName
		);
		const username = pickText(
			profile.username,
			parsed?.preferred_username,
			parsed?.username,
			previousUser?.username
		) ?? '';
		const name = pickText(
			profile.firstName || profile.lastName ? `${profile.firstName ?? ''} ${profile.lastName ?? ''}` : null,
			fullNameFromParts,
			parsed?.name,
			previousUser?.name,
			username
		) ?? username;

		const user: User = {
			id: pickText(profile.id, parsed?.sub, previousUser?.id) ?? '',
			username,
			email: pickText(profile.email, parsed?.email, previousUser?.email) ?? undefined,
			name,
			firstName,
			lastName,
			displayName: pickText(name, previousUser?.displayName, username),
			avatarUrl: pickAvatar(previousUser?.avatarUrl),
			workspaceAvatarUrl: pickAvatar(previousUser?.workspaceAvatarUrl),
			legacyAvatarUrl: pickAvatar(previousUser?.legacyAvatarUrl),
			tenantId,
			roles,
			// Los permisos efectivos vienen del backend (/permissions/me); el JWT no decide autorización.
			permissions: currentPerms,
			allowedSystems: previousUser?.allowedSystems ?? [],
			profileSyncedAt: previousUser?.profileSyncedAt
		};

		authStore.setAuthenticated(true);
		authStore.setUser(user);
		authStore.setToken(token);

		// ⚠️ IMPORTANTE: El SSO original de Keycloak no inyecta los permisos granulares
		// que viven en la base de datos de PostgreSQL en nuestro `permissions: parsed?.permissions`.
		// Necesitamos hacer polling a /auth/me para que `user.permissions` se rellene.
		if (token) {
			await loadUserInfo(token).catch(() => { });
		}

		if (tenantChanged && browser) {
			try {
				const { companyStore } = await import('./stores/company.svelte');
				companyStore.clear();
			} catch { }
		}

		previousTenantId = tenantId;
	} catch (err) {
		console.error('[auth] Error actualizando estado:', err);
		authStore.reset();
	}
};

// ─────────────────────────────────────────────────────────
// Keycloak JS token hooks (solo para el flujo SSO)
// ─────────────────────────────────────────────────────────

/**
 * Configura los callbacks de Keycloak JS para notificar al SessionManager
 * sobre cambios de token y eventos de sesión SSO.
 */
const setupKeycloakTokenHooks = () => {
	if (!keycloakInstance) return;

	keycloakInstance.onTokenExpired = () => {
		keycloakInstance
			?.updateToken(70)
			.then((refreshed) => {
				if (refreshed && keycloakInstance?.token) {
					authStore.setToken(keycloakInstance.token);
					import('./session-manager')
						.then(({ getSessionManager }) => {
							getSessionManager()?.updateToken(keycloakInstance!.token!);
						})
						.catch(() => { });
				}
			})
			.catch(() => {
				console.error('[auth] No se pudo refrescar el token de Keycloak');
				void logout();
			});
	};

	keycloakInstance.onAuthRefreshSuccess = () => {
		if (keycloakInstance?.token) authStore.setToken(keycloakInstance.token);
	};

	keycloakInstance.onAuthRefreshError = () => {
		console.error('[auth] Error en refresh de Keycloak — cerrando sesión');
		void logout();
	};

	keycloakInstance.onAuthLogout = () => {
		authStore.reset();
	};
};

// ─────────────────────────────────────────────────────────
// Login
// ─────────────────────────────────────────────────────────

/** Inicia sesión con Keycloak (OAuth redirect flow) */
export const loginWithKeycloak = async (tenantSlug?: string) => {
	if (!keycloakInstance) {
		console.error('[auth] Keycloak no está inicializado');
		return;
	}
	const options: any = { redirectUri: window.location.origin + '/callback' };
	if (tenantSlug) options.loginHint = tenantSlug;
	await keycloakInstance.login(options);
};

/**
 * Login con usuario/contraseña (legacy — el login principal es via form action del servidor).
 * Los tokens se guardan en cookies no-HttpOnly (fragmentadas si hace falta) y en memoria (authStore).
 * NO se guardan en localStorage.
 */
export const login = async (credentials: {
	username: string;
	password: string;
	tenant_slug: string;
}): Promise<{ success: boolean; error?: string; data?: any }> => {
	try {
		const { api } = await import('./api');
		const response = await api.auth.login(credentials);

		if (response.error) {
			return { success: false, error: response.error };
		}

		const loginData = response.data;
		if (loginData?.access_token) {
			// Guardar en memoria y en cookie no-HttpOnly para SSR
			authStore.setToken(loginData.access_token);
			authStore.setAuthenticated(true);
			setAccessTokenOnDocument(loginData.access_token);
			// El refresh_token llega en cookie HttpOnly desde el servidor.
			// NO lo guardamos en JS.
			await loadUserInfo(loginData.access_token);
		}

		return { success: true, data: loginData };
	} catch (err) {
		console.error('[auth] Error en login:', err);
		return { success: false, error: 'Error de conexión con el servidor' };
	}
};

// ─────────────────────────────────────────────────────────
// User info
// ─────────────────────────────────────────────────────────

/**
 * Permisos efectivos RBAC de la app para la compañía (backend: GET .../permissions/me).
 * Sin esto, `userHasPermission` solo ve lo que venga en /auth/me del Hub.
 */
export async function syncCompanyPermissions(companyId: number): Promise<void> {
	if (!browser || !Number.isFinite(companyId)) return;

	// Pre-popular desde localStorage para que el sidebar renderice sin parpadeo
	const preUserId = get(authStore).user?.id;
	if (preUserId) {
		const cached = loadSidebarPermsCache(preUserId, companyId);
		if (cached) {
				const s = get(authStore);
				if (s.user) {
					const mergedRoles = Array.from(new Set([...(s.user.roles ?? []), ...cached.roles]));
					authStore.setUser({
						...s.user,
						permissions: cached.permissions,
						roles: mergedRoles,
						allowedSystems:
							cached.allowedSystems.length > 0
								? (cached.allowedSystems as import('./stores/system.svelte').SystemType[])
								: s.user.allowedSystems,
						tenantId: cached.tenantId ?? s.user.tenantId
					});
					permissionsHydrated.set(true);
				}
			}
	}

	try {
		const { api } = await import('./api');
		const res = await api.get<{
			permissions: string[];
			roles?: string[];
			allowed_systems?: string[];
			tenant_id?: number | null;
		}>(`/v1/core/permissions/me?company_id=${companyId}`);
		if (res.error || res.data === undefined) return;
		const perms = res.data.permissions;
		if (!Array.isArray(perms)) return;
		const state = get(authStore);
		if (!state.user) return;
		const { systemStore } = await import('./stores/system.svelte');
		const allowedSystems = (res.data.allowed_systems ?? []) as import('./stores/system.svelte').SystemType[];
		const rolesFromBackend = Array.isArray(res.data.roles) ? res.data.roles : null;
		const tenantFromBackend =
			typeof res.data.tenant_id === 'number' && Number.isFinite(res.data.tenant_id)
				? res.data.tenant_id
				: null;

		// Merge no destructivo de roles: preservamos los roles atestados por el
		// Hub (p. ej. `admin` del realm Keycloak en /v1/auth/me) y agregamos los
		// roles locales devueltos por el backend (`super_admin`, etc.). Si los
		// roles locales sobrescribieran a los del Hub, el bypass de admin se
		// rompería entre la primera hidratación y la sincronización por compañía.
		const previousRoles = state.user.roles ?? [];
		const mergedRoles =
			rolesFromBackend === null
				? previousRoles
				: Array.from(new Set([...previousRoles, ...rolesFromBackend]));

		// El backend es fuente de verdad para permisos de la compañía activa.
		// Si retornó [] es porque el usuario realmente no tiene permisos aquí;
		// el guard de setUser preservaría los viejos (incorrectos). Por eso unsafe.
		authStore.setUserUnsafe({
			...state.user,
			permissions: perms,
			roles: mergedRoles,
			tenantId: tenantFromBackend ?? state.user.tenantId,
			// Preservar allowedSystems del SSR si el API no los retorna (seed pendiente)
			allowedSystems: allowedSystems.length > 0 ? allowedSystems : (state.user.allowedSystems ?? [])
		});

		// Solo reinicializar el systemStore si el API retorna sistemas explícitos.
		// Si está vacío, preservar el estado establecido por SSR para evitar resetear activeSystem a null.
		if (allowedSystems.length > 0) {
			const cookieSystem = typeof document !== 'undefined'
				? document.cookie.match(/(?:^|;\s*)active_system=([^;]+)/)?.[1] ?? null
				: null;
			systemStore.initialize(allowedSystems, cookieSystem);
		}

		// Persistir en localStorage para que el sidebar pre-popule sin parpadeo en la próxima sesión
		const freshUserId = get(authStore).user?.id;
		if (freshUserId) {
			saveSidebarPermsCache(freshUserId, companyId, {
				permissions: perms,
				roles: mergedRoles,
				allowedSystems: allowedSystems.length > 0
					? (allowedSystems as string[])
					: ((get(authStore).user?.allowedSystems ?? []) as string[]),
				tenantId: tenantFromBackend ?? get(authStore).user?.tenantId
			});
		}
	} catch (e) {
		console.warn('[auth] syncCompanyPermissions:', e);
	} finally {
		// Levanta el flag aunque la sync falle: si no se pudo, las pantallas
		// quedan con lo que vino del SSR y deben dejar de mostrar el loader.
		permissionsHydrated.set(true);
	}
}

export const refreshPermissions = async (): Promise<boolean> => {
	const token = getToken();
	if (!token) return false;

	permissionsRefreshing.set(true);
	try {
		// RBAC de compañía primero: evita que /auth/me vacíe permisos antes del sync real.
		try {
			const { companyStore } = await import('./stores/company.svelte');
			const cid = companyStore.activeCompany?.id;
			if (cid) await syncCompanyPermissions(cid);
		} catch {
			// ignore
		}
		await loadUserInfo(token);
		return true;
	} finally {
		permissionsRefreshing.set(false);
	}
};

const loadUserInfo = async (token: string) => {
	try {
		authStore.setToken(token);
		const { api } = await import('./api');
		const response = await api.auth.me();
		if (response.data) {
			const previousUser = get(authStore).user;
			const d = response.data;
			const workspaceAvatarUrl = pickAvatar(
				d.workspaceAvatarUrl,
				d.workspace_avatar_url,
				d.avatar_url,
				d.avatarUrl,
				d.picture,
				d.photo,
				previousUser?.workspaceAvatarUrl
			);
			const legacyAvatarUrl = pickAvatar(
				d.legacyAvatarUrl,
				d.legacy_avatar_url,
				d.avatar,
				d.photo,
				d.picture,
				previousUser?.legacyAvatarUrl
			);
			const avatarUrl = pickAvatar(workspaceAvatarUrl, legacyAvatarUrl, previousUser?.avatarUrl);
			
			// Merge no destructivo: nunca pisar datos válidos con campos vacíos
			const firstName = pickText(d.first_name, d.firstName, previousUser?.firstName);
			const lastName = pickText(d.last_name, d.lastName, previousUser?.lastName);
			const username = pickText(
				d.preferred_username,
				d.username,
				previousUser?.username
			) ?? '';
			const nameFromParts = pickText(
				firstName && lastName ? `${firstName} ${lastName}` : null,
				firstName,
				lastName
			);
			const name = pickText(
				d.name,
				nameFromParts,
				previousUser?.name,
				username
			) ?? username;
			const displayName = pickText(
				d.displayName,
				d.display_name,
				name,
				username
			) ?? username;
			const email = pickText(d.email, previousUser?.email) ?? undefined;
			const userId = pickText(d.sub, d.id, previousUser?.id) ?? '';
			
			console.debug('[avatar][auth.loadUserInfo] /v1/auth/me avatar_url recibido:', workspaceAvatarUrl ?? '(null)');
			console.debug('[avatar][auth.loadUserInfo] avatar final para authStore:', avatarUrl ?? '(null)');
			console.debug('[profile][auth.loadUserInfo] first_name:', firstName ?? '(null)', 'last_name:', lastName ?? '(null)', 'avatar_url:', workspaceAvatarUrl ?? '(null)');
			
			authStore.setUser({
				id: userId,
				username,
				email,
				name,
				firstName,
				lastName,
				displayName,
				avatarUrl,
				workspaceAvatarUrl,
				legacyAvatarUrl,
				tenantId: d.tenant_id ?? previousUser?.tenantId,
				roles: mergeRolesFromHub(d.roles, previousUser?.roles),
				permissions: mergePermissionsFromHub(d.permissions, previousUser?.permissions),
				allowedSystems: previousUser?.allowedSystems ?? [],
				profileSyncedAt: Date.now()
			});
		}
	} catch (err) {
		console.error('[auth] Error cargando info del usuario:', err);
	}
};

/**
 * Sincroniza el perfil del usuario desde /v1/auth/me
 * - Valida cache TTL (5 minutos) antes de hacer fetch
 * - Extrae first_name, last_name, avatar_url
 * - Retorna objeto con campos de perfil para UI o update de store
 * 
 * Uso:
 * ```
 * const profile = await syncUserProfile(accessToken);
 * if (profile) {
 *   // profile.firstName, profile.lastName, profile.displayName, profile.avatarUrl
 * }
 * ```
 */
export const syncUserProfile = async (accessToken?: string): Promise<{
	firstName: string | null;
	lastName: string | null;
	displayName: string | null;
	avatarUrl: string | null;
	workspaceAvatarUrl: string | null;
	legacyAvatarUrl: string | null;
	rawProfileSyncedAt: number;
} | null> => {
	if (!browser) return null;

	try {
		// Validar cache TTL: 5 minutos (300000ms)
		const PROFILE_CACHE_TTL = 5 * 60 * 1000;
		const { get } = await import('svelte/store');
		const currentState = get(authStore);
		const now = Date.now();
		
		if (
			currentState.user?.profileSyncedAt &&
			(now - currentState.user.profileSyncedAt) < PROFILE_CACHE_TTL
		) {
			console.debug('[profile][sync] Cache válido, no re-fetching /v1/auth/me');
			return {
				firstName: currentState.user.firstName ?? null,
				lastName: currentState.user.lastName ?? null,
				displayName: currentState.user.displayName ?? null,
				avatarUrl: currentState.user.avatarUrl ?? null,
				workspaceAvatarUrl: currentState.user.workspaceAvatarUrl ?? null,
				legacyAvatarUrl: currentState.user.legacyAvatarUrl ?? null,
				rawProfileSyncedAt: currentState.user.profileSyncedAt
			};
		}

		const token = accessToken || getToken();
		if (!token) {
			console.warn('[profile][sync] No token disponible para sincronizar');
			return null;
		}

		// Llamar a loadUserInfo que hace fetch a /v1/auth/me
		await loadUserInfo(token);
		
		// Retornar los nuevos valores desde el store
		const updatedState = get(authStore);
		if (updatedState.user) {
			console.debug('[profile][sync] Perfil sincronizado exitosamente');
			return {
				firstName: updatedState.user.firstName ?? null,
				lastName: updatedState.user.lastName ?? null,
				displayName: updatedState.user.displayName ?? null,
				avatarUrl: updatedState.user.avatarUrl ?? null,
				workspaceAvatarUrl: updatedState.user.workspaceAvatarUrl ?? null,
				legacyAvatarUrl: updatedState.user.legacyAvatarUrl ?? null,
				rawProfileSyncedAt: updatedState.user.profileSyncedAt ?? 0
			};
		}

		return null;
	} catch (err) {
		console.error('[profile][sync] Error sincronizando perfil:', err);
		return null;
	}
};

// ─────────────────────────────────────────────────────────
// Logout
// ─────────────────────────────────────────────────────────

export const logout = async () => {
	if (!browser) return;

	try {
		// Detener el SessionManager
		try {
			const { destroySessionManager } = await import('./session-manager');
			destroySessionManager();
		} catch { }

		// Limpiar store de compañías
		try {
			const { companyStore } = await import('./stores/company.svelte');
			companyStore.clear();
		} catch { }

		// Limpiar cache de permisos del sidebar en localStorage
		try {
			const userId = get(authStore).user?.id;
			clearSidebarPermsCache(userId);
		} catch { }

		// Limpiar snapshot visual del sidebar almacenado en sessionStorage
		try {
			sessionStorage.removeItem('app:sidebar:nav-main:v1');
		} catch {
			// ignore
		}

		// Limpiar estado en memoria
		authStore.reset();
		persistUserInSession(null);

		clearAccessTokenOnDocument();
		// La cookie HttpOnly del refresh_token la limpia el servidor

		// Logout unificado (SSO y password): POST al logout route del servidor.
		// Evita redirección visible al endpoint de Keycloak.
		if (keycloakInstance) {
			try {
			keycloakInstance.clearToken();
			} catch {}
		}

		const form = document.createElement('form');
		form.method = 'POST';
		form.action = '/logout';
		document.body.appendChild(form);
		form.submit();
	} catch (err) {
		console.error('[auth] Error durante logout:', err);
		const hubBase = (import.meta.env.VITE_HUB_URL || 'https://workspace.aduanasoft.com').replace(/\/+$/, '');
		window.location.href = `${hubBase}/login?return_to=${encodeURIComponent(window.location.origin + '/login?sso_verified=1')}`;
	}
};

// ─────────────────────────────────────────────────────────
// Token accessors
// ─────────────────────────────────────────────────────────

export const hasRole = (role: string): boolean => {
	if (!keycloakInstance?.authenticated) return false;
	return keycloakInstance.hasRealmRole(role);
};

/** Obtiene el access token desde memoria (Keycloak JS o authStore) */
export const getToken = (): string | null => {
	// Prioridad 1: Keycloak JS en memoria
	if (keycloakInstance?.token) return keycloakInstance.token;

	// Prioridad 2: authStore en memoria
	let token: string | null = null;
	const unsub = authStore.subscribe((s) => { token = s.token; });
	unsub();
	if (token) return token;

	// Prioridad 3: cookie no-HttpOnly (fallback para acceso inicial antes del onMount)
	if (browser) return getAccessTokenFromDocument();

	return null;
};

/**
 * Refresca el access token usando el endpoint server-side seguro.
 * El servidor lee el refresh_token de la cookie HttpOnly.
 * @returns true si el refresh fue exitoso
 */
export const refreshAccessToken = async (): Promise<boolean> => {
	if (!browser) return false;

	// Con Keycloak JS activo, usar su mecanismo nativo
	if (keycloakInstance?.authenticated) {
		try {
			const refreshed = await keycloakInstance.updateToken(70);
			if (refreshed || keycloakInstance.token) {
				authStore.setToken(keycloakInstance.token ?? null);
				return true;
			}
		} catch {
			await logout();
			return false;
		}
	}

	// Flujo de contraseña: usar el endpoint server-side seguro
	try {
		const resp = await fetch('/api-sveltekit/auth/silent-refresh', {
			method: 'POST',
			credentials: 'include',
			headers: { 'Content-Type': 'application/json' }
		});

		if (!resp.ok) {
			await logout();
			return false;
		}

		const data = await resp.json() as { access_token?: string };
		if (data.access_token) {
			authStore.setToken(data.access_token);
			setAccessTokenOnDocument(data.access_token);
			return true;
		}
	} catch (err) {
		console.error('[auth] Error en refreshAccessToken:', err);
	}

	await logout();
	return false;
};

/** @deprecated El refresh_token ya no se expone en JS. */
export const getRefreshToken = (): string | null => {
	console.warn('[auth] getRefreshToken() está deprecado — el refresh_token no se expone en JS.');
	return null;
};

export const getKeycloakInstance = (): Keycloak | null => keycloakInstance;
