/* app.jsx — shell: router por hash, nav, layout, Error Boundary, theme. Monta CR.screens.*
   Se carga ÚLTIMO (después de libs + ui + screens). Babel/JSX. */
(function () {
  window.CR = window.CR || {};
  window.CR.screens = window.CR.screens || {};
  var React = window.React, ReactDOM = window.ReactDOM;
  var h = React.createElement;
  var useState = React.useState, useEffect = React.useEffect;

  // íconos del design system (SVG profesionales en assets/icons/app/)
  function Ico(p) {
    return h('img', { className: p.className || 'ico', src: 'assets/icons/app/' + (p.name || 'dashboard') + '.svg', alt: '' });
  }
  window.CR.Ico = Ico;

  // Una misma identidad y botonera para ambos módulos; no concede permisos.
  function ModuleSidebar(props) {
    var side = React.useRef(null);
    var mobileState = useState(window.innerWidth <= 900), mobile = mobileState[0];
    useEffect(function () {
      function resize() { mobileState[1](window.innerWidth <= 900); }
      window.addEventListener('resize', resize);
      return function () { window.removeEventListener('resize', resize); };
    }, []);
    useEffect(function () { if (side.current) side.current.inert = mobile && !props.open; }, [mobile, props.open]);
    useEffect(function () {
      if (!mobile || !props.open || !side.current) return;
      var before = document.activeElement;
      side.current.querySelector('.sidebar-close').focus();
      return function () { if (before && before.isConnected) before.focus(); };
    }, [mobile, props.open]);
    function menuKeys(ev) {
      if (!mobile || !props.open || ev.key !== 'Tab') return;
      var items = Array.from(side.current.querySelectorAll('a[href],button:not(:disabled)')).filter(function (el) { return el.offsetParent !== null; });
      if (!items.length) return;
      if (ev.shiftKey && document.activeElement === items[0]) { ev.preventDefault(); items[items.length - 1].focus(); }
      if (!ev.shiftKey && document.activeElement === items[items.length - 1]) { ev.preventDefault(); items[0].focus(); }
    }
    return h(React.Fragment, null,
      props.open ? h('button', { className: 'sidebar-backdrop', type: 'button',
        'aria-label': 'Cerrar menú', tabIndex: -1, onClick: props.onClose }) : null,
      h('aside', { className: 'sidebar' + (props.open ? ' open' : ''), id: 'module-sidebar', ref: side,
        role: mobile && props.open ? 'dialog' : undefined, 'aria-modal': mobile && props.open ? 'true' : undefined,
        'aria-label': 'Menú de ' + props.name, 'aria-hidden': mobile && !props.open ? 'true' : undefined, onKeyDown: menuKeys },
        h('div', { className: 'brand' },
          h('img', { className: 'brand-mark', src: 'assets/icons/app/' + props.icon + '.svg', alt: '', width: 44, height: 44 }),
          h('div', { className: 'brand-copy' }, h('b', null, props.name),
            h('small', null, props.subtitle),
            h('span', { className: 'brand-version' }, 'v' + ((window.CR_VERSION || {}).version || '1.0.6')))),
        h('button', { type: 'button', className: 'sidebar-close sidebar-action', onClick: props.onClose },
          h('span', { 'aria-hidden': 'true' }, '×'), h('span', null, 'Cerrar menú')),
        props.children,
        h('div', { className: 'sidebar-actions', 'aria-label': 'Preferencias y sesión' },
          h('a', { className: 'sidebar-action sidebar-action--module', href: '#/' },
            h(Ico, { name: 'modulos' }), h('span', null, 'Cambiar de módulo'),
            h('span', { className: 'sidebar-action__arrow', 'aria-hidden': 'true' }, '→')),
          h('button', { type: 'button', className: 'sidebar-action', onClick: props.onTheme },
            h(Ico, { name: props.theme === 'light' ? 'moon' : 'sun' }),
            h('span', null, props.theme === 'light' ? 'Modo oscuro' : 'Modo claro')),
          /* «🆕 Novedades» (v1.0.13): reabre el popup de novedades de ESTE módulo, con un puntito si hay
             alguna sin ver. Si components/novedades.jsx no cargó, el menú queda como antes. */
          props.novedades && window.CR.Novedades && window.CR.novedades
            ? h(window.CR.Novedades.Boton, { modulo: props.novedades, session: props.session, onAntes: props.onClose }) : null,
          h('button', { type: 'button', className: 'sidebar-action', onClick: props.onPassword },
            h(Ico, { name: 'key' }), h('span', null, 'Cambiar mi contraseña')),
          h('button', { type: 'button', className: 'sidebar-action sidebar-action--logout', onClick: props.onLogout },
            h(Ico, { name: 'logout' }), h('span', null, 'Cerrar sesión')))));
  }
  window.CR.ModuleSidebar = ModuleSidebar;

  // Nav del PRODUCTO (cliente): UN solo flujo claro para no confundir. La pantalla "Rutas MELI (copiloto)"
  // ya incluye importar + asignar + ajustar + discrepancias + confirmar + notificar + KPIs. Las pantallas
  // demo (dashboard/importar/flota/asignacion/notificar/seguimiento/kpis) quedan registradas pero FUERA del
  // menú (accesibles por hash para uso interno) — antes se colaban y el cliente entraba al módulo equivocado.














  /* EL DASHBOARD VIVE EN `#/brains`, NO EN `#/` (v1.0.13). Desde el selector de módulos (v0.76.0,
     `a0e6cfd`) la ruta vacía ES el selector: `modulo` da null y el shell dibuja «Tu operación, en un
     lugar.» antes de mirar NAV. El Dashboard se había quedado con el `route: ''` de la demo, así que
     «Dashboard del día» en el menú ponía `#/` y sacaba al operador del módulo — desde otra pantalla,
     y también tocándolo con el Dashboard ya abierto (y el «Atrás» del navegador volvía a caer ahí).
     Entrando por la tarjeta del selector sí se veía: `#/brains` no era de nadie y caía en `inicio`.
     Por eso convivían el reporte («lleva al selector») y la captura con el Dashboard abierto.
     Reproducido en Chrome el 27/09; lo fija `harness/verify-nav-dashboard.cjs`.
     `brains` es la MISMA dirección que la tarjeta del selector: una sola para la pantalla principal.
     Un rol sin `dashboard` (Liquidaciones) que entra por `#/brains` sigue cayendo en su `inicio`,
     porque `permitido` mira la sección del candidato. `#/` sigue siendo el selector («Cambiar de
     módulo», «← Elegir módulo»). */
  var NAV = [
    { route: 'estado-negocio', seccion: 'estado-negocio', name: 'Estado de Negocio', key: 'EstadoNegocio', icon: 'kpi' },
    { route: 'brains', seccion: 'dashboard', name: 'Dashboard del día', key: 'DashboardReal', icon: 'dashboard' },
    { route: 'reportes', seccion: 'reportes', name: 'Reportes (histórico)', key: 'Reportes', icon: 'kpi' },
    { route: 'kpi-antiguedad', seccion: 'kpi-antiguedad', name: 'Antigüedad de flota', key: 'KpiAntiguedad', icon: 'kpi' },
    { route: 'tarifas-meli', seccion: 'tarifas-meli', name: 'Tarifas Mercado Libre', key: 'TarifasMeli', icon: 'kpi' },
    { route: 'tarifas-proveedor', seccion: 'tarifas-proveedor', name: 'Tarifa de proveedores', key: 'TarifasProveedor', icon: 'kpi' },
    { route: 'asignacion-real', seccion: 'asignacion-real', name: 'Asignar rutas del día (MELI)', key: 'AsignacionReal', icon: 'asignacion' },





    { route: 'last-mile', modulo: 'lastMile', name: 'Asignar rutas Last Mile', key: null, icon: 'asignacion', nota: 'módulo en desarrollo' },
    { route: 'tablero-acks', seccion: 'tablero-acks', name: 'ACKs de WhatsApp', key: 'TableroAcks', icon: 'whatsapp' },
    { route: 'flota-real', seccion: 'flota-real', name: 'Flota (MELI)', key: 'FlotaReal', icon: 'flota' },
    { route: 'onboarding-grupos', seccion: 'onboarding-grupos', name: 'Onboarding de grupos', key: 'OnboardingGrupos', icon: 'whatsapp' },




    { route: 'historial', seccion: 'historial', name: 'Módulo de Liquidaciones', key: 'Historial', icon: 'timeline' },
    { route: 'duenos-agencia', seccion: 'duenos-agencia', name: 'Dueños de Agencia', key: 'DuenosAgencia', icon: 'flota' },
    { route: 'usuarios', seccion: 'usuarios', name: 'Usuarios', key: 'Usuarios', icon: 'flota' },
    { route: 'auditoria', seccion: 'auditoria', name: 'Auditoría', key: 'Auditoria', icon: 'timeline' }
  ];
  // pantallas internas (fuera del nav): se resuelven por hash (#/importar, etc.) pero no se muestran al cliente
  var HIDDEN = [
    { route: 'importar', name: 'Importar pedidos (demo)', key: 'Importar', icon: 'import' },
    { route: 'flota', name: 'Flota & Choferes (demo)', key: 'Flota', icon: 'flota' },
    { route: 'dashboard', name: 'Dashboard (demo)', key: 'Dashboard', icon: 'dashboard' },
    { route: 'asignacion', name: 'Asignación (demo)', key: 'Asignacion', icon: 'asignacion' },
    { route: 'notificar', name: 'Notificar (demo)', key: 'Notificar', icon: 'whatsapp' },
    { route: 'seguimiento', name: 'Seguimiento (demo)', key: 'Seguimiento', icon: 'timeline' },
    { route: 'kpis', name: 'KPIs (demo)', key: 'Kpis', icon: 'kpi' }
  ];

  /* El front YA NO tiene su propia tabla de permisos. Las secciones vienen resueltas del servidor
     en `/me` (`session.secciones`), que es el mismo dato con el que el servidor niega los pedidos.
     Dos tablas siempre terminan derivando, y la deriva se ve como un menu que ofrece algo que
     despues da 403, o que esconde algo permitido.
     Sin `secciones` no se asume nada: se muestra lo minimo. Un backend viejo no puede convertirse
     en un permiso ancho por accidente. */
  function puedeSeccion(session, seccion) {
    var permitidas = (session && session.secciones) || ['dashboard', 'reportes'];
    return permitidas.indexOf(seccion) !== -1;
  }
  /* Un item del menú es una PANTALLA (tiene `seccion`, decide el servidor por sección) o un ENLACE A
     OTRO MÓDULO (tiene `modulo`, decide `session.modulos`). Las dos preguntas se hacen en un solo
     lugar para que el menú no pueda ofrecer un módulo al que la sesión no entra. */
  function puedeItem(session, n) {
    if (n.modulo) return !!(session && session.modulos && session.modulos[n.modulo]);
    return puedeSeccion(session, n.seccion);
  }
  function navPara(session) { return NAV.filter(function (n) { return puedeItem(session, n); }); }

  /* Costura para el arnes: sin esto, verificar que el menu respete los permisos obliga a montar la
     app entera con todas las pantallas stubbeadas, y un test tan caro no se corre. Exponer la tabla
     y el predicado es barato y deja que `harness/verify-roles-nav.cjs` los ejercite de verdad.
     No es una puerta: sobreescribir esto en el navegador cambia lo que se DIBUJA, no lo que el
     servidor autoriza — el gate real esta del otro lado y no mira nada de aca. */
  window.CR.permisos = { NAV: NAV, HIDDEN: HIDDEN, puedeSeccion: puedeSeccion, puedeItem: puedeItem, navPara: navPara };

  function getRoute() { return (window.location.hash || '#/').replace(/^#\/?/, ''); }

  // Error Boundary (anti pantalla-blanca)
  function ErrorBoundary(props) { return props.children; }
  var EB = (function () {
    return class extends React.Component {
      constructor(p) { super(p); this.state = { err: null }; }
      static getDerivedStateFromError(err) { return { err: err }; }
      componentDidCatch(err, info) { console.error('[CR] screen error', err, info); }
      render() {
        if (this.state.err) {
          return h('div', { className: 'card', style: { margin: 24, borderColor: 'var(--danger)' } },
            h('h3', { style: { color: 'var(--danger)' } }, 'Se rompió esta pantalla'),
            h('p', { className: 'muted' }, String(this.state.err && this.state.err.message || this.state.err)));
        }
        return this.props.children;
      }
    };
  })();

  function App() {
    window.CR.useStore();   // el dock de CRÁNEO lee la ruta seleccionada desde el store
    var rState = useState(getRoute());
    var route = rState[0], setRoute = rState[1];
    var modulo = !route ? null : route.indexOf('last-mile') === 0 ? 'last-mile' : 'brains';
    var openState = useState(false); var open = openState[0], setOpen = openState[1];
    var themeState = useState(localStorage.getItem('cr.theme') || 'dark');
    var theme = themeState[0], setTheme = themeState[1];
    var beState = useState(false); var backendOn = beState[0], setBackendOn = beState[1];
    var sessionState = useState(undefined); var session = sessionState[0], setSession = sessionState[1];
    var sessionErrorState = useState(''); var sessionError = sessionErrorState[0], setSessionError = sessionErrorState[1];


    var claveState = useState(false); var verClave = claveState[0], setVerClave = claveState[1];
    var dockState = useState(localStorage.getItem('cr.copiloto') !== 'off' && window.innerWidth > 1100);
    var dock = dockState[0], setDock = dockState[1];
    function setDockPersist(v) { setDock(v); localStorage.setItem('cr.copiloto', v ? 'on' : 'off'); }

    useEffect(function () {
      if (!window.CR.backend) { setSession(null); return; }
      window.CR.backend.init().then(function (on) {
        setBackendOn(!!on);
        if (!on) { setSessionError('No pude conectar con el servidor.'); setSession(null); return null; }
        return window.CR.backend.me();
      }).then(function (actual) {
        if (!actual) { setSession(null); return; }
        setSession(actual);
      }).catch(function () { setSessionError('No pude validar la sesión. Reintentá.'); setSession(null); });
    }, []);

    /* `cargando` lo mira SÓLO el popup de novedades (v1.0.13): no se abre mientras llega la jornada.
       El contador descarta el final de una carga vieja (otra sesión u otro módulo). */
    var cargaState = useState(false); var cargando = cargaState[0], setCargando = cargaState[1];
    var cargaRef = React.useRef(0);
    useEffect(function () {
      if (session && modulo === 'brains' && (!session.modulos || session.modulos.brains)
          && !(session.usuario && session.usuario.mustChangePassword)) {
        var mia = ++cargaRef.current;
        setCargando(true);
        Promise.resolve(window.CR.backend.cargarSesion()).catch(function () {})
          .then(function () { if (cargaRef.current === mia) setCargando(false); });
      }
    }, [session, modulo]);

    useEffect(function () {
      function onHash() { setRoute(getRoute()); setOpen(false); }
      function onCopilotoOpen() { setDock(true); localStorage.setItem('cr.copiloto', 'on'); }
      // Escape cierra el dock (clave en pantallas chicas, donde flota encima del contenido)
      function onKey(e) { if (e.key === 'Escape') { setOpen(false); setDock(function (d) { if (d) localStorage.setItem('cr.copiloto', 'off'); return false; }); } }
      window.addEventListener('hashchange', onHash);
      window.addEventListener('cr-copiloto-open', onCopilotoOpen);
      window.addEventListener('keydown', onKey);
      return function () {
        window.removeEventListener('hashchange', onHash);
        window.removeEventListener('cr-copiloto-open', onCopilotoOpen);
        window.removeEventListener('keydown', onKey);
      };
    }, []);

    useEffect(function () {
      document.documentElement.setAttribute('data-theme', theme);
      localStorage.setItem('cr.theme', theme);
    }, [theme]);

    if (!modulo) return h(window.CR.screens.Modulos, { session: session });
    if (session === undefined) return h('div', { className: 'session-loading', role: 'status' }, 'Validando sesión…');
    if (!session) return h(window.CR.screens.Login, { modulo: modulo, error: sessionError, onLogin: function (actual) {
      setSessionError(''); setSession(actual);
    } });

    /* Corral de contraseña provisoria (v0.39.0). El servidor devuelve 403 en todo salvo /me,
       /me/password, /logout y /health mientras la marca siga puesta: sin esta rama el operador
       vería la app entera rota en vez de una instrucción. Va ANTES de calcular nav y pantalla,
       porque cualquier pantalla que se monte va a disparar pedidos que el backend rechaza. */
    if (session.usuario && session.usuario.mustChangePassword) {
      return h(window.CR.screens.CambiarClave, {
        obligatorio: true,
        // El servidor revoca todas las sesiones al cambiarla, incluida la de esta pestaña: se
        // vuelve al login en vez de dejar una app con una cookie que ya no vale.
        onListo: function () { window.CR.session = null; setSession(null); }
      });
    }

    /* Cambio VOLUNTARIO. Existe porque dos mensajes del producto —el 409 `usa_cambio_propio` del
       servidor y el title del botón de resetear en la fila propia— mandan al usuario a "Cambiar mi
       contraseña", y hasta acá esa pantalla sólo se abría dentro del corral: el producto instruía
       algo imposible. Lo encontró la revisión adversaria del 12/08. */
    if (verClave) {
      return h(window.CR.screens.CambiarClave, {
        obligatorio: false,
        onListo: function () { setVerClave(false); window.CR.session = null; setSession(null); },
        onCancelar: function () { setVerClave(false); }
      });
    }

    if (modulo === 'last-mile') {
      // Flota es el padrón compartido: conserva capacidades de la sesión, no las del rol Last Mile.
      window.CR.session = session;
      window.CR.canWrite = !!(session.caps && session.caps.mutate);
      window.CR.canSeeCosts = !!(session.caps && session.caps.viewCosts);
      window.CR.canSeePii = !!(session.caps && session.caps.pii === 'full');
      return h(EB, { key: 'last-mile' }, h(window.CR.screens.LastMile, { session: session,
        vista: route === 'last-mile/flota' ? 'flota' : route === 'last-mile/tarifas' ? 'tarifas' : route === 'last-mile/liquidacion' ? 'liquidacion' : 'operacion',
        theme: theme, onTheme: function () { setTheme(theme === 'dark' ? 'light' : 'dark'); },
        menuOpen: open, onMenu: function () { setOpen(!open); }, onCloseMenu: function () { setOpen(false); },
        backendOn: backendOn,
        onPassword: function () { setVerClave(true); setOpen(false); },
        onLogout: function () { return window.CR.backend.logout().then(function () { window.CR.session = null; setSession(null); }); }
      }));
    }
    if (session.modulos && !session.modulos.brains) return h('main', { className: 'login-shell' },
      h('div', { className: 'card login-card' }, h('h1', null, 'Cuenta de Last Mile'),
        h('p', null, 'Esta cuenta tiene acceso a Last Mile.'), h('a', { className: 'btn btn--primary', href: '#/last-mile' }, 'Abrir Last Mile')));

    var rol = session.rol;
    window.CR.session = session;
    /* Las capacidades tambien salen del servidor. Antes se recalculaban comparando contra
       'gerente_logistica', y con los roles nuevos eso miente: Coordinador y Liquidaciones SI ven
       costos. Igual que las secciones, el que decide es uno solo. */
    var caps = session.caps || { mutate: false, viewCosts: false, pii: 'none' };
    window.CR.canWrite = !!caps.mutate;
    window.CR.canSeeCosts = !!caps.viewCosts;
    window.CR.canSeePii = caps.pii === 'full';
    var navVisible = navPara(session);
    var candidate = NAV.find(function (n) { return n.key && n.route === route; }) || HIDDEN.find(function (n) { return n.route === route; });
    /* Una pantalla HIDDEN (las demo, por hash) no tiene seccion: se permite solo si el rol puede
       escribir. Sin esta rama, `candidate.seccion` es undefined y `indexOf` da -1 para todos. */
    var permitido = candidate && (candidate.seccion ? puedeSeccion(session, candidate.seccion) : !!caps.mutate);





    var inicio = navVisible.find(function (n) { return n.key && n.seccion === 'dashboard'; })
      || navVisible.find(function (n) { return !!n.key; });
    var active = permitido ? candidate : inicio;
    var Screen = window.CR.screens[active.key];
    var rutaSel = (window.CR.store && window.CR.store.list('copilotoRuta')[0]) || null;

    var dockVisible = dock && !!caps.mutate;
    return h('div', { className: 'app' + (dockVisible ? ' app--dock' : ''), 'data-role': rol },
      h(ModuleSidebar, { open: open, onClose: function () { setOpen(false); },
        icon: 'craneo-rutas', name: 'CRÁNEO RUTAS', subtitle: 'BRAINS Logística', theme: theme,
        session: session, novedades: 'rutas',
        onTheme: function () { setTheme(theme === 'dark' ? 'light' : 'dark'); },
        onPassword: function () { setVerClave(true); setOpen(false); },
        onLogout: function () { window.CR.backend.logout().finally(function () { window.CR.session = null; setSession(null); }); } },
        h('nav', { className: 'sidebar-nav', 'aria-label': 'Navegación de Cráneo Rutas' }, navVisible.map(function (n) {
          return h('button', { key: n.route, className: 'navlink' + (n.route === active.route ? ' active' : ''),
            title: n.nota ? n.name + ' — ' + n.nota : undefined,
            onClick: function () { window.location.hash = '#/' + n.route; } },
            h(Ico, { name: n.icon }), h('span', null, n.name),

            n.nota ? h('span', { className: 'badge badge--neutral', style: { marginLeft: 6 } }, n.nota) : null);
        }))),
      h('div', { className: 'main' },
        h('div', { className: 'topbar' },
          h('button', { className: 'btn btn--ghost btn--sm hamb', 'aria-label': 'Abrir menú',
            'aria-expanded': open, 'aria-controls': 'module-sidebar', onClick: function () { setOpen(!open); } }, '☰'),
          h('h1', null, active.name),
          h('div', { className: 'spacer' }),
          backendOn
            ? h('span', { className: 'badge badge--ok', title: 'Conexión con el servidor de BRAINS' }, 'Sistema conectado')
            : h('span', { className: 'badge badge--neutral' }, 'Sin conexión'),
          caps.mutate ? h('button', { className: 'btn btn--ghost btn--sm', 'aria-pressed': dock,
            title: dock ? 'Ocultar el panel de CRÁNEO' : 'Abrir el panel de CRÁNEO',
            onClick: function () { setDockPersist(!dock); } }, dock ? '🧠 CRÁNEO ▸' : '🧠 CRÁNEO') : null),
        h('div', { className: 'content' },
          h(EB, { key: active.key },
            Screen ? h(Screen, { session: session, rol: rol, readOnly: !caps.mutate }) :
              h('div', { className: 'empty' }, h('h3', null, 'Módulo en construcción'),
                h('p', { className: 'muted' }, 'La pantalla "' + active.name + '" todavía no cargó.'))))),
      // dock derecho fijo: CRÁNEO acompaña en TODAS las pantallas (misma jerarquía que el nav izquierdo)
      dockVisible ? h('aside', { className: 'copdock', 'aria-label': 'Panel de CRÁNEO' },
        h('div', { className: 'copdock__head' },
          h('span', { className: 'muted', style: { fontSize: 12 } },
            rutaSel ? ('Ruta en foco: ' + rutaSel.idRuta) : 'Elegí una ruta con "¿Por qué?"'),
          h('button', { type: 'button', className: 'copdock__x', 'aria-label': 'Cerrar el panel de CRÁNEO',
            onClick: function () { setDockPersist(false); } }, '✕')),
        h(EB, { key: 'copdock' }, h(window.CR.Copiloto, { ruta: rutaSel }))) : null,
      /* POPUP DE NOVEDADES (v1.0.13). Vive en todas las pantallas del módulo (así «🆕 Novedades» lo
         abre desde cualquiera), pero SOLO se abre en la principal: la de aterrizaje de ESTE usuario
         (`inicio`: el Dashboard, o su primera pantalla si su rol no lo tiene), pasada la carga. */
      window.CR.Novedades && window.CR.novedades ? h(window.CR.Novedades.Host, { modulo: 'rutas', session: session,
        ruta: route, principal: active === inicio, cargando: cargando }) : null);
  }
  window.CR.App = App;

  function mount() {
    window.CR.store.seedIfEmpty();
    var root = document.getElementById('root');
    ReactDOM.createRoot(root).render(h(App, null));
  }
  // Babel transpila async: esperamos a que todo esté listo
  if (document.readyState === 'complete') mount();
  else window.addEventListener('load', function () { setTimeout(mount, 0); });
})();
