/*
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/router/guards.js
| Data: 2026
| Local: São Carlos/SP — Brasil
|--------------------------------------------------------------------------
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
// Nunca disparar refresh concorrente durante navegação protegida.
// Ver comentário em session.js sobre race condition.

import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { useEntitlementsStore } from '@/stores/entitlementsStore';
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore';
import { buildUpgradeUrl } from '@/utils/upgradeContext';
import { logGuard, logError, logPerf } from '@/support/supportLogger';
import { resetAjuda } from '@/composables/useAjuda';

import { useMenuStore } from '@/stores/menuStore';
import { getMenuByRole } from '@/navigation';

import { sessionUser, sessionReady, sessionRefreshing, initSession, onSessionEvent } from '@/app/session';

// ✅ separa RBAC (papel) vs Plano (upgrade)
import { denyByRole, denyByPlan } from '@/router/accessRedirects'; // (denyByPlan pode ficar, mesmo que não use aqui)

// ✅ única fonte de verdade pra normalizar role
import { normalizeRole } from '@/utils/roleNormalizer';

// cache simples (evita bater no banco em toda navegação)
let sessionUidCache = null;

// cache de saas admin por uid (pra não consultar tabela toda vez)
let saasAdminCacheUid = null;
let saasAdminCacheIsAdmin = null;

// Freemium F3c — cache do root_redirect (TTL 5min) pra não bater no DB a cada "/".
let rootRedirectCache = null;
let rootRedirectCacheAt = 0;
async function getRootRedirectCached() {
    const TTL = 5 * 60 * 1000;
    if (rootRedirectCache && Date.now() - rootRedirectCacheAt < TTL) return rootRedirectCache;
    try {
        const { data, error } = await supabase.rpc('get_root_redirect');
        if (error) throw error;
        rootRedirectCache = data === 'login' ? 'login' : 'landing';
        rootRedirectCacheAt = Date.now();
    } catch {
        rootRedirectCache = 'landing'; // fallback seguro
    }
    return rootRedirectCache;
}

// V#6 — cache de globalRole por uid com TTL.
// Antes era invalidado apenas em SIGNED_IN/SIGNED_OUT, ficando stale se a role
// mudasse durante a sessão. TTL de 5min força re-fetch periódico.
const GLOBAL_ROLE_TTL_MS = 5 * 60 * 1000;
let globalRoleCacheUid = null;
let globalRoleCache = null;
let globalRoleCacheAt = 0;

// Flags module-level para garantir single-bind (ES modules são singletons,
// então basta uma variável aqui — não precisa poluir window).
let guardsBound = false;
let authListenerBound = false;

// -----------------------------------------
// Pending invite (Modelo B) — retomada pós-login
// -----------------------------------------
const PENDING_INVITE_TOKEN_KEY = 'pending_invite_token_v1';

function readPendingInviteToken() {
    try {
        return sessionStorage.getItem(PENDING_INVITE_TOKEN_KEY);
    } catch (_) {
        return null;
    }
}

function clearPendingInviteToken() {
    try {
        sessionStorage.removeItem(PENDING_INVITE_TOKEN_KEY);
    } catch (_) { }
}

// Exportadas pra permitir teste unitário sem montar o router inteiro.
export function isUuid(v) {
    return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(v || ''));
}

export function roleToPath(role) {
    // ✅ clínica: aceita nomes canônicos e legado
    if (role === 'clinic_admin' || role === 'tenant_admin' || role === 'admin') return '/admin';

    if (role === 'therapist') return '/therapist';

    // ✅ supervisor (papel de tenant)
    if (role === 'supervisor') return '/supervisor';

    // ⚠️ legado (se ainda existir em algum lugar)
    if (role === 'patient') return '/portal';
    if (role === 'portal_user') return '/portal';

    // ✅ saas master
    if (role === 'saas_admin') return '/saas';

    return '/';
}

function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

async function waitSessionIfRefreshing() {
    if (!sessionReady.value) {
        try {
            await initSession({ initial: true });
        } catch (e) {
            logGuard('[guards] initSession falhou', { error: e?.message });
        }
    }

    for (let i = 0; i < 30; i++) {
        if (!sessionRefreshing.value) return;
        await sleep(50);
    }
}

async function isSaasAdmin(uid) {
    if (!uid) return false;

    if (saasAdminCacheUid === uid && typeof saasAdminCacheIsAdmin === 'boolean') {
        return saasAdminCacheIsAdmin;
    }

    const { data, error } = await supabase.from('profiles').select('role').eq('id', uid).single();

    const ok = !error && data?.role === 'saas_admin';

    saasAdminCacheUid = uid;
    saasAdminCacheIsAdmin = ok;

    return ok;
}

// heurística segura: carrega entitlements se ainda não carregou ou mudou tenant
export function shouldLoadEntitlements(ent, tenantId) {
    if (!tenantId) return false;

    const loaded = typeof ent.loaded === 'boolean' ? ent.loaded : false;
    const entTenantId = ent.activeTenantId ?? ent.tenantId ?? null;

    if (!loaded) return true;
    if (entTenantId && entTenantId !== tenantId) return true;
    return false;
}

// wrapper: chama loadForTenant sem depender de force:false existir
async function loadEntitlementsSafe(ent, tenantId, force) {
    if (!ent?.loadForTenant) return;

    try {
        await ent.loadForTenant(tenantId, { force: !!force });
    } catch (e) {
        // se quebrou tentando force false (store não suporta), tenta força true uma vez
        if (!force) {
            logGuard('[guards] ent.loadForTenant force fallback', { error: e?.message });
            await ent.loadForTenant(tenantId, { force: true });
            return;
        }
        throw e;
    }
}

/**
 * wrapper: tenant features store pode não aceitar force:false (ou pode falhar silenciosamente)
 * -> tenta sem forçar e, se der ruim, tenta force:true.
 */
async function fetchTenantFeaturesSafe(tf, tenantId) {
    if (!tf?.fetchForTenant) return;
    try {
        await tf.fetchForTenant(tenantId, { force: false });
    } catch (e) {
        logGuard('[guards] tf.fetchForTenant force fallback', { error: e?.message });
        await tf.fetchForTenant(tenantId, { force: true });
    }
}

// util: roles guard (plural) com aliases
export function matchesRoles(roles, activeRole) {
    if (!Array.isArray(roles) || !roles.length) return true;

    const ar = normalizeRole(activeRole);
    const wanted = roles.map(normalizeRole);

    return wanted.includes(ar);
}

// ======================================================
// ✅ MENU: monta 1x por contexto (sem flicker)
// - O AppMenu lê menuStore.model e não recalcula.
// ======================================================

// V#9 router — skip-fast: evita useMenuStore() + comparações quando o último
// key processado é o mesmo. Reset em SIGNED_OUT/SIGNED_IN garante invalidação.
let lastEnsureKey = null;

async function ensureMenuBuilt({ uid, tenantId, tenantRole, globalRole }) {
    try {
        const isSaas = globalRole === 'saas_admin';
        const roleForMenu = isSaas ? 'saas_admin' : normalizeRole(tenantRole);

        // ✅ FIX: inclui o role normalizado E o tenantId no key de forma explícita
        // O bug era: em alguns fluxos tenantRole chegava vazio/antigo antes de
        // setActiveTenant() ser chamado, fazendo o key bater com o menu errado.
        const safeRole = roleForMenu || 'unknown';
        const safeTenant = tenantId || 'no-tenant';
        const safeGlobal = globalRole || 'no-global';
        const key = `${uid}:${safeTenant}:${safeRole}:${safeGlobal}`;

        // V#9 — short-circuit: mesmo key da última chamada → menu já construído nessa nav
        if (lastEnsureKey === key) return;

        const menuStore = useMenuStore();

        // ✅ FIX PRINCIPAL: só considera cache válido se role E tenant baterem.
        // Antes, o check era feito antes de garantir que tenant.activeRole
        // já tinha sido resolvido corretamente nessa navegação.
        if (menuStore.ready && menuStore.key === key && Array.isArray(menuStore.model) && menuStore.model.length > 0) {
            // sanity check extra: verifica se o modelo tem itens do role correto
            // (evita falso positivo quando key colide por acidente)
            const firstLabel = menuStore.model?.[0]?.label || '';
            const isClinicMenu = firstLabel === 'Clínica';
            const isTherapistMenu = firstLabel === 'Terapeuta';
            const isSupervisorMenu = firstLabel === 'Supervisão';
            const isEditorMenu = firstLabel === 'Editor';
            const isPortalMenu = firstLabel === 'Paciente';
            const isSaasMenuCached = firstLabel === 'SaaS';

            const expectClinic = safeRole === 'clinic_admin';
            const expectTherapist = safeRole === 'therapist';
            const expectSupervisor = safeRole === 'supervisor';
            const expectEditor = safeRole === 'editor';
            const expectPortal = safeRole === 'patient';
            const expectSaas = safeRole === 'saas_admin';

            const menuMatchesRole =
                (expectClinic && isClinicMenu) ||
                (expectTherapist && isTherapistMenu) ||
                (expectSupervisor && isSupervisorMenu) ||
                (expectEditor && isEditorMenu) ||
                (expectPortal && isPortalMenu) ||
                (expectSaas && isSaasMenuCached) ||
                // roles desconhecidos: aceita o cache (coreMenu)
                (!expectClinic && !expectTherapist && !expectSupervisor && !expectEditor && !expectPortal && !expectSaas);

            if (menuMatchesRole) {
                lastEnsureKey = key;
                return; // cache válido e menu correto
            }

            // cache com key igual mas menu errado: força rebuild
            logGuard('[ensureMenuBuilt] menu incompatível com role, forçando rebuild', { key, safeRole, firstLabel });
            menuStore.reset();
        }

        // garante tenant_features pronto ANTES de construir
        if (!isSaas && tenantId) {
            const tfm = useTenantFeaturesStore();
            const hasAny = tfm?.features && typeof tfm.features === 'object' && Object.keys(tfm.features).length > 0;
            const loadedFor = tfm?.loadedForTenantId || null;
            if (!hasAny || (loadedFor && loadedFor !== tenantId)) {
                await fetchTenantFeaturesSafe(tfm, tenantId);
            } else if (!loadedFor) {
                await fetchTenantFeaturesSafe(tfm, tenantId);
            }
        }

        const tfm2 = useTenantFeaturesStore();
        const ctx = {
            isSaasAdmin: isSaas,
            tenantLoading: () => false,
            tenantFeaturesLoading: () => false,
            tenantFeatureEnabled: (featureKey) => {
                if (!tenantId) return false;
                try {
                    return !!tfm2.isEnabled(featureKey, tenantId);
                } catch {
                    return false;
                }
            },
            role: () => normalizeRole(tenantRole)
        };

        const model = getMenuByRole(roleForMenu, ctx) || [];
        menuStore.setMenu(key, model);
        lastEnsureKey = key;
    } catch (e) {
        logGuard('[guards] ensureMenuBuilt failed', { error: e?.message });
    }
}

// V#9 — invalida o short-circuit do ensureMenuBuilt (chamado em SIGNED_IN/OUT)
function resetEnsureMenuKey() {
    lastEnsureKey = null;
}

export function applyGuards(router) {
    if (guardsBound) return;
    guardsBound = true;

    router.beforeEach(async (to) => {
        const tlabel = `[guard] ${to.fullPath}`;
        const _perfEnd = logPerf('router.guard', tlabel);

        try {
            // ==========================================
            // ✅ AUTH SEMPRE LIBERADO (blindagem total)
            // (ordem importa: /auth antes de meta.public)
            // ==========================================
            if (to.path.startsWith('/auth')) {
                _perfEnd();
                return true;
            }

            // ==========================================
            // ✅ Rotas públicas
            // ==========================================
            if (to.meta?.public) {
                _perfEnd();
                return true;
            }

            // ✅ Freemium F3c: raiz "/" do visitante NÃO-logado vai pra landing
            // (/lp) ou login conforme root_redirect (config saas). Logado segue
            // pro fluxo normal (HomeCards).
            if (to.path === '/') {
                await waitSessionIfRefreshing();
                if (!sessionUser.value?.id) {
                    const target = await getRootRedirectCached();
                    _perfEnd();
                    return target === 'login' ? { path: '/auth/login' } : { path: '/lp' };
                }
            }

            // se rota não exige auth, libera
            if (!to.meta?.requiresAuth) {
                _perfEnd();
                return true;
            }

            // não decide nada no meio do refresh do session.js
            logGuard('waitSessionIfRefreshing');
            await waitSessionIfRefreshing();

            // precisa estar logado (fonte estável do session.js)
            const uid = sessionUser.value?.id || null;
            if (!uid) {
                sessionStorage.setItem('redirect_after_login', to.fullPath);
                _perfEnd();
                return { path: '/auth/login' };
            }

            const isTenantArea =
                to.path.startsWith('/admin') ||
                to.path.startsWith('/therapist') ||
                to.path.startsWith('/supervisor') ||
                to.path.startsWith('/configuracoes');

            // ======================================
            // ✅ IDENTIDADE GLOBAL (cached por uid — sem query a cada navegação)
            // - se falhar, NÃO nega por engano: volta pro login (seguro)
            // ======================================
            let globalRole = null;

            const cacheAge = Date.now() - globalRoleCacheAt;
            const cacheValid = globalRoleCacheUid === uid && globalRoleCache && cacheAge < GLOBAL_ROLE_TTL_MS;

            if (cacheValid) {
                globalRole = globalRoleCache;
                logGuard('profiles.role (cache) =', globalRole);
            } else {
                const { data: prof, error: profErr } = await supabase.from('profiles').select('role').eq('id', uid).single();

                globalRole = !profErr ? prof?.role : null;
                if (globalRole) {
                    globalRoleCacheUid = uid;
                    globalRoleCache = globalRole;
                    globalRoleCacheAt = Date.now();
                }
                logGuard('profiles.role (db) =', globalRole);
            }

            if (!globalRole) {
                sessionStorage.setItem('redirect_after_login', to.fullPath);
                _perfEnd();
                return { path: '/auth/login' };
            }

            // ======================================
            // ✅ TRAVA GLOBAL: portal_user não entra em tenant-app
            // ======================================
            if (isTenantArea && globalRole === 'portal_user') {
                // limpa lixo de tenant herdado
                try {
                    localStorage.removeItem('tenant_id');
                    localStorage.removeItem('tenant');
                    localStorage.removeItem('currentTenantId');
                } catch (_) { }

                _perfEnd();
                return { path: '/portal' };
            }

            // ======================================
            // ✅ Portal (identidade global) via meta.profileRole
            // ======================================
            if (to.meta?.profileRole) {
                if (globalRole !== to.meta.profileRole) {
                    _perfEnd();
                    return { path: '/pages/access' };
                }

                // monta menu do portal (patient) antes de liberar
                await ensureMenuBuilt({
                    uid,
                    tenantId: null,
                    tenantRole: globalRole, // ex.: 'portal_user'
                    globalRole
                });

                _perfEnd();
                return true;
            }

            // ======================================
            // ✅ ÁREA GLOBAL (não-tenant)
            // - /account/* é perfil/config do usuário
            // - NÃO pode carregar tenantStore nem trocar contexto de tenant
            // ======================================
            const isAccountArea = to.path === '/account' || to.path.startsWith('/account/');
            if (isAccountArea) {
                // Garante menu + entitlements ao recarregar diretamente em /account/* (ex.: F5).
                // globalRole (profiles.role) não mapeia para menus reais → precisamos da tenant role.
                const _menuStore = useMenuStore();
                if (!_menuStore.ready) {
                    try {
                        const _tStore = useTenantStore();
                        if (!_tStore.activeRole) {
                            await _tStore.loadSessionAndTenant();
                        }
                        const _role = _tStore.activeRole;
                        const _tid = _tStore.activeTenantId || null;
                        if (_role && _tid) {
                            // Carrega entitlements do tenant (mesma lógica do guard principal)
                            const _ent = useEntitlementsStore();
                            if (shouldLoadEntitlements(_ent, _tid)) {
                                await loadEntitlementsSafe(_ent, _tid, true);
                            }
                            // Entitlements pessoais — subscription do user independe do role
                            // no tenant ativo (tenant_admin do próprio tenant pode ter therapist_pro)
                            if (_ent.loadedForUser !== uid) {
                                try {
                                    await _ent.loadForUser(uid);
                                } catch { }
                            }
                            await ensureMenuBuilt({ uid, tenantId: _tid, tenantRole: _role, globalRole });
                        }
                    } catch { }
                }
                _perfEnd();
                return true;
            }

            // (opcional, mas recomendado)
            // se não é tenant_member, evita carregar tenant/entitlements sem necessidade
            if (globalRole && globalRole !== 'tenant_member') {
                try {
                    localStorage.removeItem('tenant_id');
                    localStorage.removeItem('tenant');
                    localStorage.removeItem('currentTenantId');
                } catch (_) { }
            }

            // ==========================================
            // ✅ Pending invite (Modelo B)
            // ==========================================
            const pendingInviteToken = readPendingInviteToken();

            if (pendingInviteToken && !isUuid(pendingInviteToken)) {
                clearPendingInviteToken();
            }

            if (pendingInviteToken && isUuid(pendingInviteToken) && !to.path.startsWith('/accept-invite')) {
                _perfEnd();
                return { path: '/accept-invite', query: { token: pendingInviteToken } };
            }

            // se uid mudou, invalida caches e stores dependentes
            if (sessionUidCache !== uid) {
                sessionUidCache = uid;
                saasAdminCacheUid = null;
                saasAdminCacheIsAdmin = null;
                globalRoleCacheUid = null;
                globalRoleCache = null;
                globalRoleCacheAt = 0;

                const ent0 = useEntitlementsStore();
                if (typeof ent0.invalidate === 'function') ent0.invalidate();

                const tf0 = useTenantFeaturesStore();
                if (typeof tf0.invalidate === 'function') tf0.invalidate();

                try {
                    const menuStore = useMenuStore();
                    if (typeof menuStore.reset === 'function') menuStore.reset();
                } catch { }
            }

            // ================================
            // ✅ SAAS MASTER: não depende de tenant
            // ================================
            if (to.meta?.saasAdmin) {
                logGuard('isSaasAdmin');
                // usa identidade global primeiro (evita cache fantasma)
                const ok = globalRole === 'saas_admin' ? true : await isSaasAdmin(uid);
                if (!ok) {
                    _perfEnd();
                    return { path: '/pages/access' };
                }

                // ✅ monta menu SaaS 1x (AppMenu lê do menuStore)
                await ensureMenuBuilt({
                    uid,
                    tenantId: null,
                    tenantRole: 'saas_admin',
                    globalRole
                });

                _perfEnd();
                return true;
            }

            // ================================
            // ✅ ÁREA DO EDITOR (papel de plataforma)
            // Verificado por platform_roles[] em profiles, não por tenant.
            // ⚠️ Requer migration: ALTER TABLE profiles ADD COLUMN platform_roles text[] DEFAULT '{}'
            // ================================
            if (to.meta?.editorArea) {
                let platformRoles = [];
                try {
                    const { data: pRoles } = await supabase.from('profiles').select('platform_roles').eq('id', uid).single();
                    platformRoles = Array.isArray(pRoles?.platform_roles) ? pRoles.platform_roles : [];
                } catch {
                    // coluna ainda não existe: acesso negado por padrão
                }

                if (!platformRoles.includes('editor')) {
                    _perfEnd();
                    return { path: '/pages/access' };
                }

                await ensureMenuBuilt({
                    uid,
                    tenantId: null,
                    tenantRole: 'editor',
                    globalRole
                });

                _perfEnd();
                return true;
            }

            // ================================
            // 🚫 SaaS master: bloqueia tenant-app por padrão
            // ✅ Mas libera rotas de DEMO em DEV
            // ================================
            logGuard('saas.lockdown?');

            // 🔥 PATCH: aqui NÃO consulta isSaasAdmin — usa só identidade global
            const isSaas = globalRole === 'saas_admin';

            if (isSaas) {
                // V#10 — usa meta.area como fonte primária; path.startsWith vira fallback
                // pra rotas legacy que ainda não declaram meta.area. matchedRouteHasArea()
                // checa a cadeia inteira de matched (lida com routes aninhadas).
                const matchedHasSaasArea = (to.matched || []).some((r) => r.meta?.area === 'saas' || r.meta?.saasAdmin === true);
                const matchedHasDemoArea = (to.matched || []).some((r) => r.meta?.area === 'demo');
                const isSaasArea = matchedHasSaasArea || to.path === '/saas' || to.path.startsWith('/saas/');

                // Rotas do Tema Demo (em DEV)
                const isDemoArea = import.meta.env.DEV && (matchedHasDemoArea || to.path === '/demo' || to.path.startsWith('/demo/'));

                // Se for demo em DEV, libera
                if (isDemoArea) {
                    // ✅ ainda assim monta menu SaaS (pra layout não piscar)
                    await ensureMenuBuilt({
                        uid,
                        tenantId: null,
                        tenantRole: 'saas_admin',
                        globalRole
                    });

                    _perfEnd();
                    return true;
                }

                // Fora de /saas (e não-demo), não pode
                if (!isSaasArea) {
                    _perfEnd();
                    return { path: '/saas' };
                }

                // ✅ estamos no /saas: monta menu SaaS
                await ensureMenuBuilt({
                    uid,
                    tenantId: null,
                    tenantRole: 'saas_admin',
                    globalRole
                });
            }

            // ================================
            // ✅ Abaixo daqui é tudo tenant-app
            // ================================

            // carrega tenant + role
            const tenant = useTenantStore();
            logGuard('tenant.loadSessionAndTenant?');
            if (!tenant.loaded && !tenant.loading) {
                await tenant.loadSessionAndTenant();
            }

            // se não tem user no store, trata como não logado
            if (!tenant.user) {
                sessionStorage.setItem('redirect_after_login', to.fullPath);
                _perfEnd();
                return { path: '/auth/login' };
            }

            // se não tem tenant ativo:
            if (!tenant.activeTenantId) {
                const mem = Array.isArray(tenant.memberships) ? tenant.memberships : [];

                // 1) tenta casar role da rota (ex.: therapist) com membership
                const wantedRoles = Array.isArray(to.meta?.roles) ? to.meta.roles : [];
                const wantedNorm = wantedRoles.map(normalizeRole);

                const preferred = wantedNorm.length ? mem.find((m) => m && m.status === 'active' && m.tenant_id && wantedNorm.includes(normalizeRole(m.role, m.kind))) : null;

                // 2) fallback: primeiro active
                const firstActive = preferred || mem.find((m) => m && m.status === 'active' && m.tenant_id);

                if (!firstActive) {
                    // ✅ Freemium F2: logado mas SEM nenhum tenant ativo → /onboarding,
                    // que provisiona o plano gratuito e resolve estados (slug colidiu,
                    // pago aguardando, erro). saas_admin não passa por aqui.
                    if (globalRole !== 'saas_admin' && to.path !== '/onboarding') {
                        _perfEnd();
                        return { path: '/onboarding' };
                    }

                    // 🔥 race/sem vínculo: em área tenant, trate como login necessário (não AccessDenied)
                    if (isTenantArea) {
                        sessionStorage.setItem('redirect_after_login', to.fullPath);
                        _perfEnd();
                        return { path: '/auth/login' };
                    }

                    if (to.path === '/pages/access') {
                        _perfEnd();
                        return true;
                    }
                    _perfEnd();
                    return { path: '/pages/access' };
                }

                if (typeof tenant.setActiveTenant === 'function') {
                    tenant.setActiveTenant(firstActive.tenant_id);
                } else {
                    tenant.activeTenantId = firstActive.tenant_id;
                    tenant.activeRole = firstActive.role;
                }
            }

            // 🔥 FIX: se ainda assim não resolveu tenant/role e estamos em tenant area, não negue "por engano"
            if (isTenantArea && (!tenant.activeTenantId || !tenant.activeRole)) {
                sessionStorage.setItem('redirect_after_login', to.fullPath);
                _perfEnd();
                return { path: '/auth/login' };
            }

            let tenantId = tenant.activeTenantId;
            if (!tenantId) {
                if (to.path === '/pages/access') {
                    _perfEnd();
                    return true;
                }
                _perfEnd();
                return { path: '/pages/access' };
            }

            // =====================================================
            // ✅ tenantScope baseado em tenants.kind (fonte da verdade)
            // =====================================================
            const scope = to.meta?.tenantScope; // 'personal' | 'clinic'

            if (scope) {
                const mem = Array.isArray(tenant.memberships) ? tenant.memberships : [];

                // seleciona membership ativa cujo kind corresponde ao escopo
                const desired = mem.find((m) => m && m.status === 'active' && m.tenant_id && ((scope === 'personal' && m.kind === 'saas') || (scope === 'clinic' && m.kind === 'clinic') || (scope === 'supervisor' && m.kind === 'supervisor')));

                const desiredTenantId = desired?.tenant_id || null;

                if (desiredTenantId && tenant.activeTenantId !== desiredTenantId) {
                    logGuard(`tenantScope.switch(${scope})`);

                    // ✅ guarda o tenant antigo para invalidar APENAS ele
                    const oldTenantId = tenant.activeTenantId;

                    if (typeof tenant.setActiveTenant === 'function') {
                        tenant.setActiveTenant(desiredTenantId);
                    } else {
                        tenant.activeTenantId = desiredTenantId;
                    }

                    localStorage.setItem('tenant_id', desiredTenantId);
                    tenantId = desiredTenantId;

                    try {
                        const entX = useEntitlementsStore();
                        if (typeof entX.invalidate === 'function') entX.invalidate();
                    } catch { }

                    // ✅ NÃO invalidar o cache inteiro — invalida somente o tenant anterior
                    try {
                        const tfX = useTenantFeaturesStore();
                        if (typeof tfX.invalidate === 'function' && oldTenantId) tfX.invalidate(oldTenantId);
                    } catch { }

                    // ✅ troca tenant => menu precisa recompôr (contexto mudou)
                    try {
                        const menuStore = useMenuStore();
                        if (typeof menuStore.reset === 'function') menuStore.reset();
                    } catch { }
                } else if (!desiredTenantId) {
                    logGuard('[guards] tenantScope sem match', {
                        scope,
                        memberships: mem.map((x) => ({ tenant_id: x?.tenant_id, role: x?.role, kind: x?.kind, status: x?.status }))
                    });
                }
            }

            // se trocar tenant, invalida cache de tenant_features (evita ler feature de tenant errado)
            const tfSwitch = useTenantFeaturesStore();
            if (tfSwitch.loadedForTenantId && tfSwitch.loadedForTenantId !== tenantId) {
                // ✅ invalida só o tenant que estava carregado antes
                tfSwitch.invalidate(tfSwitch.loadedForTenantId);
            }

            // entitlements (✅ carrega só quando precisa)
            const ent = useEntitlementsStore();
            if (shouldLoadEntitlements(ent, tenantId)) {
                logGuard('ent.loadForTenant');
                await loadEntitlementsSafe(ent, tenantId, true);
            }

            // ✅ user entitlements: subscription pessoal (therapist_pro/free,
            // supervisor_*, etc) independe do role no tenant ativo — um terapeuta
            // pode ser tenant_admin do próprio tenant E ter assinatura pessoal.
            // Sempre carrega pra qualquer user autenticado.
            if (uid && ent.loadedForUser !== uid) {
                logGuard('ent.loadForUser');
                try {
                    await ent.loadForUser(uid);
                } catch (e) {
                    logGuard('[guards] ent.loadForUser failed', { error: e?.message });
                }
            }

            // ================================
            // ✅ tenant_features (módulos ativáveis por clínica)
            // meta.tenantFeature = 'patients' | ...
            // ================================
            const requiredTenantFeature = to.meta?.tenantFeature;
            if (requiredTenantFeature) {
                const tf = useTenantFeaturesStore();
                logGuard('tenantFeatures.fetchForTenant');
                await fetchTenantFeaturesSafe(tf, tenantId);

                // ✅ IMPORTANTÍSSIMO: passa tenantId
                const enabled = typeof tf.isEnabled === 'function' ? tf.isEnabled(requiredTenantFeature, tenantId) : false;

                if (!enabled) {
                    if (to.path === '/admin/clinic/features') {
                        _perfEnd();
                        return true;
                    }

                    _perfEnd();
                    return {
                        path: '/admin/clinic/features',
                        query: { missing: requiredTenantFeature, redirectTo: to.fullPath }
                    };
                }
            }

            // ------------------------------------------------
            // ✅ RBAC (roles) — BLOQUEIA se não for compatível
            // ------------------------------------------------
            const allowedRolesRaw = Array.isArray(to.meta?.roles) ? to.meta.roles : null;
            const allowedRoles = allowedRolesRaw && allowedRolesRaw.length ? allowedRolesRaw.map(normalizeRole) : null;

            const activeRoleNorm = normalizeRole(tenant.activeRole);

            if (allowedRoles && allowedRoles.length && !allowedRoles.includes(activeRoleNorm)) {
                const mem = Array.isArray(tenant.memberships) ? tenant.memberships : [];

                const compatible = mem.find((m) => m && m.status === 'active' && m.tenant_id === tenantId && allowedRoles.includes(normalizeRole(m.role, m.kind)));

                if (compatible) {
                    tenant.activeRole = normalizeRole(compatible.role, compatible.kind);
                } else {
                    _perfEnd();
                    return denyByRole({ to, currentRole: tenant.activeRole });
                }
            }

            // role guard (singular)
            const requiredRoleRaw = to.meta?.role;
            const requiredRole = requiredRoleRaw ? normalizeRole(requiredRoleRaw) : null;

            if (requiredRole && normalizeRole(tenant.activeRole) !== requiredRole) {
                _perfEnd();
                return denyByRole({ to, currentRole: tenant.activeRole });
            }

            // ------------------------------------------------
            // ✅ feature guard (entitlements/plano → upgrade)
            // ------------------------------------------------
            const requiredFeature = to.meta?.feature;
            if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
                if (to.name === 'upgrade') {
                    _perfEnd();
                    return true;
                }

                const url = buildUpgradeUrl({
                    missingKeys: [requiredFeature],
                    redirectTo: to.fullPath,
                    role: normalizeRole(tenant.activeRole) // ✅ passa o role para a UpgradePage detectar
                });

                _perfEnd();
                return url;
            }

            // ======================================================
            // ✅ MENU: monta 1x por contexto APÓS estabilizar tenant+role
            // ======================================================
            await ensureMenuBuilt({
                uid,
                tenantId,
                tenantRole: tenant.activeRole,
                globalRole
            });

            _perfEnd();
            return true;
        } catch (e) {
            logError('router.guard', 'erro no beforeEach', e);

            if (to.path.startsWith('/auth')) return true;
            if (to.meta?.public) return true;
            if (to.path === '/pages/access') return true;

            sessionStorage.setItem('redirect_after_login', to.fullPath);
            return { path: '/auth/login' };
        }
    });

    // V#2 — listener consolidado: session.js é o único registrante de
    // supabase.auth.onAuthStateChange. Aqui só nos inscrevemos via onSessionEvent.
    if (!authListenerBound) {
        authListenerBound = true;

        // SIGNED_OUT: zera caches e localStorage tenant
        onSessionEvent('SIGNED_OUT', () => {
            sessionUidCache = null;
            saasAdminCacheUid = null;
            saasAdminCacheIsAdmin = null;
            globalRoleCacheUid = null;
            globalRoleCache = null;
            globalRoleCacheAt = 0;
            resetEnsureMenuKey();

            try { resetAjuda(); } catch (_) { }

            // limpa localStorage de tenant — sem isso, próximo login restaura
            // o tenant do usuário anterior (mesma máquina)
            try {
                localStorage.removeItem('tenant_id');
                localStorage.removeItem('tenant');
                localStorage.removeItem('currentTenantId');
            } catch (_) { }

            try {
                const tf = useTenantFeaturesStore();
                if (typeof tf.invalidate === 'function') tf.invalidate();
            } catch { }

            try {
                const ent = useEntitlementsStore();
                if (typeof ent.invalidate === 'function') ent.invalidate();
            } catch { }

            try {
                const tenant = useTenantStore();
                if (typeof tenant.reset === 'function') tenant.reset();
            } catch { }

            try {
                const menuStore = useMenuStore();
                if (typeof menuStore.reset === 'function') menuStore.reset();
            } catch { }
        });

        // SIGNED_IN: só invalida se o usuário mudou de verdade
        onSessionEvent('SIGNED_IN', (sess) => {
            const uid = sess?.user?.id || null;
            if (uid && sessionUidCache && sessionUidCache === uid) return; // mesmo user

            sessionUidCache = uid || null;
            saasAdminCacheUid = null;
            saasAdminCacheIsAdmin = null;
            globalRoleCacheUid = null;
            globalRoleCache = null;
            globalRoleCacheAt = 0;
            resetEnsureMenuKey();

            try {
                const tf = useTenantFeaturesStore();
                if (typeof tf.invalidate === 'function') tf.invalidate();
            } catch { }

            try {
                const ent = useEntitlementsStore();
                if (typeof ent.invalidate === 'function') ent.invalidate();
            } catch { }

            try {
                const menuStore = useMenuStore();
                if (typeof menuStore.reset === 'function') menuStore.reset();
            } catch { }
        });

        // TOKEN_REFRESHED: não invalida nada (caso clássico de trocar de aba)
    }
}
