// gp-app.jsx — root: state, direction presets, tweaks, routing
// ───────────────────────────────────────────────────────────────
const {
  useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakToggle, TweakColor,
  Sidebar: AppSidebar, Topbar: AppTopbar,
  Dashboard: AppDashboard, EstimatorModule: AppEstimator,
  MursModule: AppMurs, FenestrationModule: AppFen, Mesure: AppMesure,
  ProjetModule: AppProjet, ConfigModule: AppConfig,
  RecettesModule: AppRecettes, SoumissionModule: AppSoumission,
  OrganisationModule: AppOrg, SystemesModule: AppSystemes, ServicesModule: AppServices, CourrielsModule: AppCourriels, DEFAULT_ORG: aDefaultOrg, DEFAULT_RECIPE_CHOICES: aDefaultChoices,
  WidgetBuilder: AppBuilder, BUILDER_WIDGETS: aBuilderWidgets, seedWidgetsFromLegacy: aSeedWidgets,
  UtilisateursModule: AppUtilisateurs,
  PRODUCT_SYSTEMS: aProductSystems,
  CategoryView: AppCategory, ModulePager,
  moduleById: aModById, PLAN_MEASURES, getSpec: aGetSpec, CUSTOM_MODULES: aCustom, ESTIMATION_MODULES: aEstMods,
  validateRegistry: aValidate, loadState: aLoad, saveState: aSave,
  Portfolio: AppPortfolio, PROJECTS: aProjects, projectById: aProjectById,
  LoginScreen: AppLogin, AwaitingScreen: AppAwaiting, ActivityModule: AppActivity, TeamModule: AppTeam,
  ACTIVITY_LOG: aActivityLog, moduleById: aModById2,
  USER_PERMS: aUserPerms0, hasCap: aHasCap, userById: aUserById,
  MODULE_GROUPS: aGroups, KnowledgeBase: AppKnowledgeBase,
  GPServices: aServices, defaultModuleConfig: aDefModCfg, emptyProjectData: aEmptyProj,
} = window;

// ── Helpers : initialiser byProject depuis un état sauvegardé ─────
function initByProject(saved) {
  // v4+ : le champ byProject est déjà présent (migration faite par gp-core)
  if (saved && saved.byProject) return saved.byProject;
  // Fallback bootstrapping pour un état neuf ou migration manquée
  const pid = (saved && saved.activeProjectId) || aProjects[0].id;
  const proj = aEmptyProj();
  if (aDefaultChoices) {
    Object.entries(aDefaultChoices).forEach(([mid, ch]) => {
      proj.moduleConfig[mid] = { ...aDefModCfg(mid), recipe: ch.recipe, brand: ch.brand };
    });
  }
  // Projet vierge par défaut : aucune mesure de démo (le relevé se fait sur plan).
  proj.measures = (saved && saved.measures) || [];
  return { [pid]: proj };
}

// Each "direction" is a coherent preset; tweaks fine-tune from there.
const DIR_PRESET = {
  a: { theme: 'light', accent: '#2f6bed', type: 'technique' },
  b: { theme: 'light', accent: '#c2603a', type: 'humaniste' }
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "direction": "a",
  "theme": "light",
  "accent": "#2f6bed",
  "type": "technique",
  "density": "regular",
  "nav": "expanded",
  "summary": "lateral"
} /*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Session : utilisateur connecté (null → écran de connexion)
  const [user, setUser] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.user) || null;
  });
  const [active, setActive] = React.useState('dashboard');
  const [unit, setUnit] = React.useState('imp');
  const [mesureOpen, setMesureOpen] = React.useState(false);

  // Le serveur fait autorité (prod) : au chargement, GET /me rapatrie rôle/statut/modules
  // à jour et remplace la copie en cache. Un simple refresh reflète donc tout changement
  // d'accès ; une session révoquée/expirée entraîne la déconnexion. No-op en mode démo.
  React.useEffect(() => {
    if (!window.GPTOAuth || !window.GPTOAuth.enabled) return;
    window.GPTOAuth.me().then(({ data }) => {
      if (data && data.user) setUser(data.user);
      else if (user) setUser(null);
    }).catch(() => {});
  }, []);

  // ── Conteneur principal par-projet ────────────────────────────
  // byProject[pid] = { measures: [...], moduleConfig: { [mid]: {...} } }
  const [byProject, setByProject] = React.useState(() => {
    const saved = aLoad && aLoad();
    return initByProject(saved);
  });

  const [savedAt, setSavedAt] = React.useState(null);
  const [activity, setActivity] = React.useState(aActivityLog);
  const [userPerms, setUserPerms] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.userPerms) || aUserPerms0;
  });
  // Permissions effectives : en prod, les overrides du compte connecté viennent du serveur
  // (user.perms) et pilotent son propre menu ; sinon la map locale (démo).
  const effPerms = React.useMemo(
    () => (user && user.perms) ? { ...userPerms, [user.id]: user.perms } : userPerms,
    [user, userPerms]);
  // Gating des modules d'estimation : en prod, un estimateur ne voit que ses modules autorisés.
  const EST_IDS = React.useMemo(() => new Set((aEstMods || []).map((m) => m.id)), []);
  const modAllowed = React.useCallback((mid) => {
    const gate = window.GPTOAuth && window.GPTOAuth.enabled && user && user.role === 'user';
    if (!gate || !EST_IDS.has(mid)) return true;
    return !!(user.modules && user.modules[mid]);
  }, [user, EST_IDS]);
  const [org, setOrg] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.org) || aDefaultOrg;
  });
  const [systems, setSystems] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.systems) || aProductSystems.map((s) => ({ ...s, picks: { ...s.picks } }));
  });
  const [widgets, setWidgets] = React.useState(() => {
    const saved = aLoad && aLoad();
    if (saved && saved.widgets) return saved.widgets;
    // Premier lancement : amorcer depuis les modules + RECIPES existants.
    return aSeedWidgets ? aSeedWidgets() : (aBuilderWidgets ? [...aBuilderWidgets] : []);
  });
  const [activeProjectId, setActiveProjectId] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.activeProjectId) || aProjects[0].id;
  });

  const project = aProjectById(activeProjectId);
  const [mesureCtx, setMesureCtx] = React.useState(null);

  // ── Sélecteurs dérivés du projet courant ──────────────────────
  const projectData = byProject[activeProjectId] || { measures: PLAN_MEASURES, moduleConfig: {} };
  const measures = projectData.measures;
  const moduleConfig = projectData.moduleConfig;

  // Compatibilité descendante : recipeChoices au sens ancien (recipe + brand + excl)
  const recipeChoices = React.useMemo(() => {
    const out = {};
    Object.entries(moduleConfig).forEach(([mid, cfg]) => {
      out[mid] = { recipe: cfg.recipe, brand: cfg.brand, excl: cfg.excl || {} };
    });
    return out;
  }, [moduleConfig]);

  // ── Injecter measureService avec accès au state ───────────────
  React.useEffect(() => {
    if (aServices && aServices.measures) {
      aServices.measures._bind(
        () => ({ byProject }),
        (updater) => setByProject(updater)
      );
    }
  }, [byProject]);

  // ── Pont take-off : le moteur GPM_ injecte ses mesures ici ────
  // Reçoit [{kind,val,unit,name,apps}] depuis gp-takeoff.js et les écrit
  // dans le projet actif, avec la cible définie à l'ouverture (__GPTO_takeoffTarget).
  React.useEffect(() => {
    window.GPTO_takeoffInject = (items) => {
      if (!items || !items.length) return 0;
      const fallback = window.__GPTO_takeoffTarget || 'toiture';
      const stamp = Date.now();
      setByProject((prev) => {
        const proj = prev[activeProjectId] || aEmptyProj();
        const added = items.map((it, i) => ({
          id: 'me' + stamp + i,
          name: it.name || `Forme ${i + 1}`,
          kind: it.kind, val: it.val, unit: it.unit,
          target: it.target || fallback, // ventilation → module dédié ; sinon module d'ouverture
          page: 1, source: 'plan',
        }));
        return { ...prev, [activeProjectId]: { ...proj, measures: [...proj.measures, ...added] } };
      });
      logAction('mesure', 'a injecté des relevés de plan', `${items.length} forme${items.length > 1 ? 's' : ''}`);
      return items.length;
    };
    return () => { try { delete window.GPTO_takeoffInject; } catch (_) {} };
  }, [activeProjectId]);

  // Validation du registre au démarrage
  React.useEffect(() => { aValidate && aValidate(); }, []);

  // ── Frontière de persistance : sauvegarde à chaque changement ──
  React.useEffect(() => {
    if (!aSave) return;
    const ts = aSave({ byProject, activeProjectId, user, userPerms, org, systems, widgets });
    if (ts) setSavedAt(ts);
  }, [byProject, activeProjectId, user, userPerms, org, systems, widgets]);

  // ── Mutateur bas niveau : patch du sous-arbre byProject[pid] ──
  const patchProject = React.useCallback((pid, patcher) => {
    setByProject(prev => {
      const cur = prev[pid] || aEmptyProj();
      return { ...prev, [pid]: patcher(cur) };
    });
  }, []);

  // ── Mesures du projet courant ─────────────────────────────────
  const setMeasures = React.useCallback((updater) => {
    patchProject(activeProjectId, (proj) => ({
      ...proj,
      measures: typeof updater === 'function' ? updater(proj.measures) : updater,
    }));
  }, [activeProjectId, patchProject]);

  // ── Configuration d'un module pour le projet courant ──────────
  const patchModuleConfig = React.useCallback((moduleId, changes) => {
    patchProject(activeProjectId, (proj) => {
      const cur = proj.moduleConfig[moduleId] || aDefModCfg(moduleId);
      return {
        ...proj,
        moduleConfig: {
          ...proj.moduleConfig,
          [moduleId]: { ...cur, ...changes },
        },
      };
    });
  }, [activeProjectId, patchProject]);

  const chooseRecipe = (moduleId, recipe, brand) => {
    patchProject(activeProjectId, (proj) => {
      const cur = proj.moduleConfig[moduleId] || aDefModCfg(moduleId);
      const excl = cur.recipe === recipe ? cur.excl || {} : {};
      return {
        ...proj,
        moduleConfig: {
          ...proj.moduleConfig,
          [moduleId]: { ...cur, recipe, brand, excl },
        },
      };
    });
  };

  const toggleExclude = (moduleId, sku) => {
    patchProject(activeProjectId, (proj) => {
      const cur = proj.moduleConfig[moduleId] || aDefModCfg(moduleId);
      const excl = { ...(cur.excl || {}), [sku]: !cur.excl?.[sku] };
      return {
        ...proj,
        moduleConfig: { ...proj.moduleConfig, [moduleId]: { ...cur, excl } },
      };
    });
    logAction('recipe', 'a ajusté un composant de recette', sku);
  };

  const applySystemAll = (sys) => {
    patchProject(activeProjectId, (proj) => {
      const nextConfig = { ...proj.moduleConfig };
      Object.entries(sys.picks).forEach(([mid, p]) => {
        const cur = nextConfig[mid] || aDefModCfg(mid);
        nextConfig[mid] = { ...cur, recipe: p.recipe, brand: p.brand, excl: {} };
      });
      return { ...proj, moduleConfig: nextConfig };
    });
    logAction('recipe', `a appliqué le système ${sys.name}`, `${Object.keys(sys.picks).length} modules`);
  };

  const login = (u) => {
    setUser(u);
    setActivity((a) => [{ id: 'a' + Date.now(), userId: u.id, type: 'login', action: 's\'est connecté(e)', cible: '', projet: '', ts: new Date().toISOString().slice(0, 16).replace('T', ' ') }, ...a]);
    setActive('dashboard');
  };
  const logout = () => { setUser(null); setActive('dashboard'); };

  const [toast, setToast] = React.useState(null);
  const toastT = React.useRef(null);
  const logAction = React.useCallback((type, action, cible = '', projet = '') => {
    setUser((u) => {
      if (u) setActivity((a) => [{ id: 'a' + Date.now(), userId: u.id, type, action, cible, projet,
        ts: new Date().toISOString().slice(0, 16).replace('T', ' ') }, ...a]);
      return u;
    });
    setToast(`${action}${cible ? ' — ' + cible : ''}`);
    clearTimeout(toastT.current);
    toastT.current = setTimeout(() => setToast(null), 2800);
  }, []);

  const setUserPermFor = (userId, cap, value) => {
    setUserPerms((prev) => {
      const next = { ...prev, [userId]: { ...(prev[userId] || {}) } };
      if (value) next[userId][cap] = true; else delete next[userId][cap];
      return next;
    });
    const tgt = aUserById && aUserById(userId);
    logAction('user', value ? 'a accordé une permission' : 'a retiré une permission',
      `${cap} — ${tgt ? tgt.prenom + ' ' + tgt.nom : userId}`);
  };

  const mod = aModById(active) || aModById('dashboard');
  const isCat = typeof active === 'string' && active.startsWith('cat:');
  const catName = isCat ? active.slice(4) : null;

  const openMesure = (ctx) => {
    // Cible par défaut des mesures injectées = module d'où l'on ouvre le take-off.
    window.__GPTO_takeoffTarget = ctx || 'toiture';
    if (typeof window.GPM_open === 'function') { window.GPM_open(); return; }
    // Repli (environnement sans moteur GPM_) : ancien overlay React.
    setMesureCtx(ctx || null); setMesureOpen(true);
  };

  const onNav = (id) => {
    if (id === 'mesure') { openMesure(null); return; }
    setActive(id);
    setMesureOpen(false);
    window.scrollTo({ top: 0 });
  };

  const setDirection = (dir) => setTweak({ direction: dir, ...DIR_PRESET[dir] });

  // ── Séquence de navigation à plat (hooks AVANT tout retour anticipé : les
  //    règles des Hooks exigent un nombre d'appels constant entre les renders,
  //    y compris la transition login → connecté). Garde sur user === null.
  const navSeq = React.useMemo(() => {
    if (!user) return [];
    const seq = [];
    aGroups.forEach((g) => g.items.forEach((m) => {
      if (m.tool) return;
      if (m.cap && !aHasCap(user, m.cap, effPerms)) return;
      if (m.superOnly && user.role !== 'super_admin') return;
      if (!modAllowed(m.id)) return;
      if (org.hidden && org.hidden[m.id]) return;
      seq.push({ id: m.id, name: m.name, cat: m.cat, icon: m.icon });
    }));
    return seq;
  }, [user, effPerms, org.hidden, modAllowed]);

  const navPrevNext = React.useMemo(() => {
    const cur = navSeq.findIndex((m) => m.id === active);
    return { cur, prev: cur > 0 ? navSeq[cur - 1] : null, next: cur >= 0 && cur < navSeq.length - 1 ? navSeq[cur + 1] : null };
  }, [navSeq, active]);

  // Garde d'authentification — APRÈS tous les hooks.
  if (!user) {
    return (
      <div className="approot" data-direction={t.direction} data-theme={t.theme}
        data-type={t.type} style={{ '--accent': t.accent, '--accent-ink': '#ffffff' }}>
        <AppLogin onLogin={login} />
      </div>
    );
  }
  // Accès sur autorisation : compte connecté mais pas encore approuvé (ou désactivé).
  // En mode démo, les profils n'ont pas de `status` → accès normal.
  if (user.status && user.status !== 'active') {
    return (
      <div className="approot" data-direction={t.direction} data-theme={t.theme}
        data-type={t.type} style={{ '--accent': t.accent, '--accent-ink': '#ffffff' }}>
        <AppAwaiting user={user} onLogout={logout} />
      </div>
    );
  }

  const switchProject = (id) => {
    // Initialiser les données du projet s'il est nouveau
    setByProject(prev => {
      if (prev[id]) return prev;
      const proj = aEmptyProj();
      if (aDefaultChoices) {
        Object.entries(aDefaultChoices).forEach(([mid, ch]) => {
          proj.moduleConfig[mid] = { ...aDefModCfg(mid), recipe: ch.recipe, brand: ch.brand };
        });
      }
      return { ...prev, [id]: proj };
    });
    setActiveProjectId(id);
    window.scrollTo({ top: 0 });
  };

  const renderModule = () => {
    if (isCat) return <AppCategory cat={catName} onNav={onNav} org={org} />;
    if ((mod.cap && !aHasCap(user, mod.cap, effPerms)) || (mod.superOnly && user.role !== 'super_admin') || !modAllowed(active)) {
      return (
        <div className="fade-in access-denied">
          <div className="empty tickbox" style={{ maxWidth: 480, margin: '40px auto' }}>
            <div className="empty-t">Accès restreint</div>
            <div className="empty-s">La section « {mod.name} » est réservée aux administrateurs. Contactez un administrateur pour obtenir les permissions.</div>
            <button className="btn" style={{ margin: '14px auto 0' }} onClick={() => onNav('dashboard')}>Retour au tableau de bord</button>
          </div>
        </div>
      );
    }
    switch (active) {
      case 'portfolio': return <AppPortfolio projects={aProjects} activeId={activeProjectId} onOpen={(id) => { switchProject(id); onNav('dashboard'); }} />;
      case 'dashboard': return <AppDashboard onNav={onNav} measures={measures} project={project} org={org} />;
      case 'projet':    return <AppProjet project={project} onLog={logAction} />;
      case 'config':    return <AppConfig onLog={logAction} />;
      case 'recettes':  return <AppRecettes onLog={logAction} systems={systems} />;
      case 'organisation': return <AppOrg org={org} onChange={setOrg} onLog={logAction} />;
      case 'builder': return <AppBuilder widgets={widgets} onChange={setWidgets} onLog={logAction} org={org} />;
      case 'courriels': return <AppCourriels user={user} onLog={logAction} />;
      case 'systemes':  return <AppSystemes systems={systems} onChange={setSystems} onLog={logAction} />;
      case 'services':  return <AppServices onLog={logAction} />;
      case 'historique': return <AppActivity user={user} activity={activity} />;
      case 'equipe':    return <AppUtilisateurs user={user} onLog={logAction} />;
      case 'soumission': return <AppSoumission
        measures={measures}
        project={project}
        onLog={logAction}
        recipeChoices={recipeChoices}
        onChoose={chooseRecipe}
        onToggleExclude={toggleExclude}
        systems={systems} />;
      case 'murs': return <AppMurs unit={unit} onMesure={() => openMesure('murs')} measures={measures} />;
      case 'fenestration': return <AppFen unit={unit} onMesure={() => openMesure('fenestration')} />;
      default: {
        const cfg = moduleConfig[active] || aDefModCfg(active);
        return <AppEstimator
          mod={mod}
          spec={aGetSpec(active)}
          unit={unit}
          onMesure={() => openMesure(mod.id)}
          measures={measures}
          moduleConfig={cfg}
          onPatchConfig={(changes) => patchModuleConfig(active, changes)}
          recipeChoices={recipeChoices}
          onChooseRecipe={chooseRecipe}
          onToggleExclude={toggleExclude}
          onApplySystem={applySystemAll}
          systems={systems} />;
      }
    }
  };

  return (
    <div
      className="approot"
      style={{ height: '100%', '--accent': t.accent, '--accent-ink': '#ffffff' }}
      data-direction={t.direction}
      data-theme={t.theme}
      data-density={t.density}
      data-type={t.type}
      data-nav={t.nav}
      data-summary={t.summary}>

      <div className="app">
        <AppSidebar activeId={mesureOpen ? 'mesure' : active} onNav={onNav} savedAt={savedAt}
          project={project} projects={aProjects} onSwitchProject={switchProject}
          user={user} onLogout={logout} userPerms={effPerms} hidden={org.hidden || {}} org={org} />
        <main className="ws">
          <AppTopbar
            crumbCat={isCat ? null : mod.cat} crumbName={isCat ? catName : mod.name}
            isHome={active === 'dashboard'} isCat={isCat}
            onHome={() => onNav('dashboard')}
            onCrumbCat={(c) => onNav('cat:' + c)}
            project={project} projects={aProjects}
            onSwitchProject={(id) => { switchProject(id); onNav('dashboard'); }}
            onPortfolio={() => onNav('portfolio')}
            direction={t.direction} onDirection={setDirection}
            theme={t.theme} onTheme={(v) => setTweak('theme', v)}
            unit={unit} onUnit={setUnit}
            onMesure={() => openMesure(!isCat && active !== 'dashboard' && !mod.system ? active : null)} />

          <div className="ws-inner">
            {!isCat && navPrevNext.cur >= 0 &&
              <ModulePager prev={navPrevNext.prev} cur={navSeq[navPrevNext.cur]} next={navPrevNext.next} onNav={onNav} />}
            {renderModule()}
          </div>
        </main>
      </div>

      {mesureOpen &&
        <AppMesure
          onClose={() => setMesureOpen(false)}
          measures={measures}
          setMeasures={setMeasures}
          ctx={mesureCtx}
          projectId={activeProjectId}
          projectName={project && project.nom}
          onExport={() => aServices && aServices.measures.exportJSON(activeProjectId, project && project.nom)}
          onImport={(file) => aServices && aServices.measures.importJSON(file).then(imported => {
            setMeasures(imported);
            logAction('mesure', 'a importé des relevés de plan', `${imported.length} formes`);
          }).catch(err => alert(err.message))} />
      }

      {AppKnowledgeBase && <AppKnowledgeBase />}

      {toast &&
        <div className="audit-toast" role="status">
          <span className="audit-toast-dot" />
          <span className="audit-toast-tx"><b>Journalisé</b> · {toast}</span>
          <button className="audit-toast-link" onClick={() => { setToast(null); onNav('historique'); }}>Voir</button>
        </div>}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Direction" />
        <TweakRadio label="Préréglage" value={t.direction}
          options={[{ value: 'a', label: 'Atelier' }, { value: 'b', label: 'Chantier' }]}
          onChange={setDirection} />

        <TweakSection label="Apparence" />
        <TweakColor label="Couleur d'accent" value={t.accent}
          options={['#2f6bed', '#c2603a', '#1f8a5b', '#7b59d6']}
          onChange={(v) => setTweak('accent', v)} />
        <TweakToggle label="Thème sombre" value={t.theme === 'dark'}
          onChange={(v) => setTweak('theme', v ? 'dark' : 'light')} />
        <TweakRadio label="Typographie" value={t.type}
          options={[{ value: 'technique', label: 'Technique' }, { value: 'humaniste', label: 'Humaniste' }, { value: 'neutre', label: 'Neutre' }]}
          onChange={(v) => setTweak('type', v)} />
        <TweakRadio label="Densité" value={t.density}
          options={[{ value: 'compact', label: 'Compact' }, { value: 'regular', label: 'Standard' }, { value: 'airy', label: 'Aéré' }]}
          onChange={(v) => setTweak('density', v)} />

        <TweakSection label="Disposition" />
        <TweakRadio label="Navigation" value={t.nav}
          options={[{ value: 'expanded', label: 'Étendue' }, { value: 'rail', label: 'Icônes' }]}
          onChange={(v) => setTweak('nav', v)} />
        <TweakRadio label="Sommaire" value={t.summary}
          options={[{ value: 'lateral', label: 'Latéral' }, { value: 'inline', label: 'En ligne' }]}
          onChange={(v) => setTweak('summary', v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
