/* =====================================================================
 * État global de l'application (contexte React synchronisé avec le serveur).
 * ===================================================================== */

const AppContext = createContext(null);
const useApp = () => useContext(AppContext);

const SYNC_DEBOUNCE_MS = 700;
const SYNC_RETRY_MS = 5000;
const POLL_INTERVAL_MS = 20000;

/** Collection d'objets `{ id, … }` : envoi des seuls éléments modifiés ou supprimés. */
const collectionAdapter = (name) => ({
  snapshot: (items) => new Map(items.map((it) => [it.id, JSON.stringify(it)])),
  diff: (snap, items) => {
    const upsert = [];
    const ids = new Set();
    items.forEach((it) => {
      ids.add(it.id);
      if (snap.get(it.id) !== JSON.stringify(it)) upsert.push(it);
    });
    const remove = [];
    snap.forEach((_, id) => {
      if (!ids.has(id)) remove.push(id);
    });
    return upsert.length || remove.length ? { upsert, delete: remove } : null;
  },
  send: (change) => apiRequest(`records/${name}`, { method: 'PUT', body: change }),
  applied: (snap, change) => {
    const next = new Map(snap);
    change.upsert.forEach((it) => next.set(it.id, JSON.stringify(it)));
    change.delete.forEach((id) => next.delete(id));
    return next;
  },
});

/** Document unique (paramètres, liste des années). */
const docAdapter = (key) => ({
  snapshot: (value) => JSON.stringify(value),
  diff: (snap, value) => (JSON.stringify(value) === snap ? null : { data: value }),
  send: (change) => apiRequest(`docs/${key}`, { method: 'PUT', body: change }),
  applied: (snap, change) => JSON.stringify(change.data),
});

/**
 * useState dont les modifications locales sont envoyées au serveur (avec délai
 * et nouvelles tentatives). Rien n'est envoyé avant le premier `load`.
 */
function useServerState(fallback, adapter, report) {
  const [value, setValue] = useState(fallback);
  const valueRef = useRef(value);
  valueRef.current = value;
  const adapterRef = useRef(adapter);
  adapterRef.current = adapter;
  const reportRef = useRef(report);
  reportRef.current = report;
  const snapshot = useRef(null);
  const timer = useRef(null);
  const busy = useRef(false);

  const schedule = (delay) => {
    clearTimeout(timer.current);
    timer.current = setTimeout(() => flushRef.current(), delay);
  };

  const flush = useCallback(async () => {
    clearTimeout(timer.current);
    timer.current = null;
    if (snapshot.current === null) return;
    if (busy.current) return schedule(SYNC_DEBOUNCE_MS);
    const change = adapterRef.current.diff(snapshot.current, valueRef.current);
    if (!change) return reportRef.current('ok');
    busy.current = true;
    reportRef.current('saving');
    try {
      await adapterRef.current.send(change);
      if (snapshot.current !== null) snapshot.current = adapterRef.current.applied(snapshot.current, change);
      busy.current = false;
      if (snapshot.current !== null && adapterRef.current.diff(snapshot.current, valueRef.current)) schedule(SYNC_DEBOUNCE_MS);
      else reportRef.current('ok');
    } catch (err) {
      busy.current = false;
      if (err.status === 401) return;
      if (isRetryableError(err)) {
        reportRef.current('error', err);
        schedule(SYNC_RETRY_MS);
      } else {
        reportRef.current('rejected', err);
      }
    }
  }, []);
  const flushRef = useRef(flush);

  useEffect(() => {
    if (snapshot.current !== null) schedule(SYNC_DEBOUNCE_MS);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [value]);

  /** Remplace l'état par celui du serveur, sauf si des modifications locales sont en attente. */
  const load = useCallback((serverValue, force = false) => {
    const pending = snapshot.current !== null && (busy.current || adapterRef.current.diff(snapshot.current, valueRef.current));
    if (pending && !force) return false;
    clearTimeout(timer.current);
    timer.current = null;
    snapshot.current = adapterRef.current.snapshot(serverValue);
    valueRef.current = serverValue;
    setValue(serverValue);
    return true;
  }, []);

  const reset = useCallback(() => {
    clearTimeout(timer.current);
    timer.current = null;
    snapshot.current = null;
    busy.current = false;
    setValue(fallback);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const isPending = useCallback(
    () => snapshot.current !== null && Boolean(busy.current || timer.current || adapterRef.current.diff(snapshot.current, valueRef.current)),
    [],
  );

  return { value, setValue, load, reset, flush, isPending };
}

function AppProvider({ children }) {
  const [session, setSession] = useState({ status: 'loading', user: null, centre: DEFAULT_SETTINGS.centre, error: null });
  const currentUser = session.user;
  const currentUserRef = useRef(currentUser);
  currentUserRef.current = currentUser;
  const [dataStatus, setDataStatus] = useState('idle');
  const [needsSetup, setNeedsSetup] = useState(false);
  const [users, setUsers] = useState([]);
  const [syncStates, setSyncStates] = useState({});
  const [legacyAvailable, setLegacyAvailable] = useState(() => Boolean(readLegacyData()));
  const [toasts, setToasts] = useState([]);
  const [printNode, setPrintNode] = useState(null);
  const [draftBilan, setDraftBilan] = useState(null);
  const draftRef = useRef(null);

  /* ---------- Notifications ---------- */

  const notify = useCallback((message, tone = 'emerald') => {
    const id = uid();
    setToasts((t) => [...t, { id, message, tone }]);
    setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3200);
  }, []);

  /* ---------- Données synchronisées ---------- */

  const refreshRef = useRef(null);
  const reporter = (name) => (state, err) => {
    setSyncStates((s) => (s[name] === state ? s : { ...s, [name]: state }));
    if (state === 'rejected') {
      notify(err.message || 'Modification refusée par le serveur', 'red');
      if (refreshRef.current) refreshRef.current(true);
    }
  };

  const bilansS = useServerState([], collectionAdapter('bilans'), reporter('bilans'));
  const jspS = useServerState([], collectionAdapter('jsp'), reporter('jsp'));
  const stockS = useServerState([], collectionAdapter('stock'), reporter('stock'));
  const appelsS = useServerState([], collectionAdapter('appels'), reporter('appels'));
  const yearsS = useServerState([SEED_JSP_YEAR], docAdapter('jspYears'), reporter('jspYears'));
  const settingsS = useServerState(DEFAULT_SETTINGS, docAdapter('settings'), reporter('settings'));
  const syncedRef = useRef([]);
  syncedRef.current = [bilansS, jspS, stockS, appelsS, yearsS, settingsS];
  const [jspYear, setJspYear] = usePersistentState('jspYear', SEED_JSP_YEAR);

  const resetData = useCallback(() => {
    syncedRef.current.forEach((s) => s.reset());
    draftRef.current = null;
    setDraftBilan(null);
    setUsers([]);
    setSyncStates({});
    setNeedsSetup(false);
    setDataStatus('idle');
  }, []);

  const refreshUsers = useCallback(async () => {
    const r = await apiRequest('users');
    setUsers(r.users);
    return r.users;
  }, []);

  const refreshData = useCallback(
    async (force = false) => {
      try {
        const data = await apiRequest('data');
        if (data.bilans) bilansS.load(data.bilans, force);
        if (data.jsp) jspS.load(data.jsp, force);
        if (data.stock) stockS.load(data.stock, force);
        if (data.appels) appelsS.load(data.appels, force);
        if ('jspYears' in data.docs) yearsS.load(data.docs.jspYears || [SEED_JSP_YEAR], force);
        settingsS.load(data.docs.settings || DEFAULT_SETTINGS, force);
        setNeedsSetup(!data.seeded);
        const me = currentUserRef.current;
        if (me && me.role === 'admin') await refreshUsers();
        setDataStatus('ready');
      } catch (err) {
        if (err.status === 401) return;
        setDataStatus((s) => (s === 'ready' ? s : 'error'));
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [refreshUsers],
  );
  refreshRef.current = refreshData;

  /* ---------- Session ---------- */

  const loadSession = useCallback(async () => {
    try {
      const r = await apiRequest('session');
      setSession({ status: 'ready', user: r.user, centre: r.centre, error: null });
    } catch (err) {
      setSession((s) => ({ ...s, status: 'ready', user: null, error: err.message }));
    }
  }, []);

  useEffect(() => {
    setUnauthorizedHandler(() => {
      if (!currentUserRef.current) return;
      resetData();
      setSession((s) => ({ ...s, user: null, error: 'Session expirée : reconnectez-vous.' }));
    });
    loadSession();
  }, [loadSession, resetData]);

  const canLoad = Boolean(currentUser && !currentUser.mustChangePassword);
  useEffect(() => {
    if (!canLoad) return undefined;
    setDataStatus('loading');
    refreshData();
    const interval = setInterval(() => refreshData(), POLL_INTERVAL_MS);
    const onVisible = () => document.visibilityState === 'visible' && refreshData();
    document.addEventListener('visibilitychange', onVisible);
    return () => {
      clearInterval(interval);
      document.removeEventListener('visibilitychange', onVisible);
    };
  }, [canLoad, currentUser && currentUser.id, refreshData]);

  useEffect(() => {
    const warn = (e) => {
      if (syncedRef.current.some((s) => s.isPending())) {
        e.preventDefault();
        e.returnValue = '';
      }
    };
    window.addEventListener('beforeunload', warn);
    return () => window.removeEventListener('beforeunload', warn);
  }, []);

  /** Retourne un message d'erreur, ou null si la connexion réussit. */
  const login = useCallback(
    async (identifiant, password) => {
      try {
        const r = await apiRequest('login', { method: 'POST', body: { identifiant: identifiant.trim(), password } });
        resetData();
        setSession((s) => ({ ...s, status: 'ready', user: r.user, error: null }));
        return null;
      } catch (err) {
        return err.message;
      }
    },
    [resetData],
  );

  const logout = useCallback(async () => {
    await Promise.all(syncedRef.current.map((s) => s.flush()));
    try {
      await apiRequest('logout', { method: 'POST' });
    } catch (err) {
      // La session locale est fermée même si le serveur est injoignable.
    }
    resetData();
    setSession((s) => ({ ...s, user: null, error: null }));
  }, [resetData]);

  const changeOwnPassword = useCallback(async (current, password) => {
    const r = await apiRequest('me/password', { method: 'POST', body: { current, password } });
    setSession((s) => ({ ...s, user: r.user }));
  }, []);

  const can = useMemo(() => {
    if (!currentUser) return { sap: false, jsp: false, admin: false };
    const admin = currentUser.role === 'admin';
    return {
      admin,
      sap: admin || Boolean(currentUser.access && currentUser.access.sap),
      jsp: admin || Boolean(currentUser.access && currentUser.access.jsp),
    };
  }, [currentUser]);

  const syncStatus = useMemo(() => {
    const states = Object.values(syncStates);
    if (states.includes('error')) return 'error';
    if (states.includes('saving')) return 'saving';
    return 'ok';
  }, [syncStates]);

  /* ---------- Initialisation du serveur & sauvegardes ---------- */

  const completeSetup = useCallback(
    async (useLegacy) => {
      const legacy = useLegacy ? readLegacyData() : null;
      const jsp = (legacy && legacy.jsp) || [...SEED_JSP_2025, ...SEED_JSP];
      const body = {
        bilans: (legacy && legacy.bilans) || [],
        jsp,
        stock: (legacy && legacy.stock) || buildStockFromJsps(jsp).stock,
        jspYears: (legacy && legacy.jspYears) || [SEED_JSP_2025_YEAR, SEED_JSP_YEAR],
        settings: (legacy && legacy.settings) || DEFAULT_SETTINGS,
        ...(legacy ? legacyUserProfiles(legacy) : {}),
      };
      await apiRequest('seed', { method: 'POST', body });
      if (legacy) {
        clearLegacyData();
        setLegacyAvailable(false);
      }
      await refreshData(true);
      await loadSession();
    },
    [refreshData, loadSession],
  );

  const importLegacy = useCallback(async () => {
    const legacy = readLegacyData();
    if (!legacy) return null;
    const data = { bilans: legacy.bilans, jsp: legacy.jsp, stock: legacy.stock, jspYears: legacy.jspYears, users: legacyUserProfiles(legacy).users };
    const r = await apiRequest('import', { method: 'POST', body: { mode: 'merge', payload: { app: 'SAP Manager', data } } });
    clearLegacyData();
    setLegacyAvailable(false);
    await refreshData(true);
    return r.counts;
  }, [refreshData]);

  const exportServerData = useCallback(() => apiRequest('export'), []);

  const loadJournal = useCallback(async (userId = '') => {
    const query = userId ? `?user=${encodeURIComponent(userId)}` : '';
    return apiRequest(`audit${query}`);
  }, []);

  const importServerData = useCallback(
    async (payload) => {
      const r = await apiRequest('import', { method: 'POST', body: { mode: 'replace', payload } });
      await refreshData(true);
      return r.counts;
    },
    [refreshData],
  );

  /* ---------- Impression ---------- */

  const print = useCallback((node) => {
    setPrintNode(node);
    setTimeout(() => window.print(), 250);
  }, []);

  useEffect(() => {
    const clear = () => setPrintNode(null);
    window.addEventListener('afterprint', clear);
    return () => window.removeEventListener('afterprint', clear);
  }, []);

  /* ---------- Bilans SAP ---------- */

  const bilans = bilansS.value;
  const setBilans = bilansS.setValue;

  const createBilan = useCallback(() => {
    const b = newBilan({ numero: nextBilanNumero(bilans), auteur: currentUser });
    draftRef.current = b;
    setDraftBilan(b);
    return b.id;
  }, [bilans, currentUser]);

  const updateBilan = useCallback(
    (id, updater) => {
      const draft = draftRef.current;
      const stamp = (b) => ({ ...updater(b), updatedAt: new Date().toISOString() });
      if (draft && draft.id === id) {
        const next = stamp(draft);
        draftRef.current = null;
        setDraftBilan(null);
        setBilans((list) => [next, ...list.filter((b) => b.id !== id)]);
        return;
      }
      setBilans((list) => list.map((b) => (b.id === id ? stamp(b) : b)));
    },
    [setBilans],
  );

  const deleteBilan = useCallback((id) => setBilans((list) => list.filter((b) => b.id !== id)), [setBilans]);

  const duplicateBilan = useCallback(
    (id) => {
      const src = bilans.find((b) => b.id === id);
      if (!src) return null;
      const now = new Date().toISOString();
      const copy = {
        ...JSON.parse(JSON.stringify(src)),
        id: uid(),
        numero: nextBilanNumero(bilans),
        status: 'brouillon',
        createdAt: now,
        updatedAt: now,
        auteurId: currentUser ? currentUser.id : null,
      };
      setBilans((list) => [copy, ...list]);
      return copy.id;
    },
    [bilans, currentUser, setBilans],
  );

  /* ---------- JSP ---------- */

  const jsps = jspS.value;
  const appels = appelsS.value;
  const setJsps = jspS.setValue;
  const setJspYears = yearsS.setValue;

  const upsertJsp = useCallback(
    (jsp) =>
      setJsps((list) => (list.some((j) => j.id === jsp.id) ? list.map((j) => (j.id === jsp.id ? jsp : j)) : [...list, jsp])),
    [setJsps],
  );

  const updateJspField = useCallback(
    (id, key, value) => setJsps((list) => list.map((j) => (j.id === id ? { ...j, [key]: value } : j))),
    [setJsps],
  );

  const deleteJsp = useCallback((id) => setJsps((list) => list.filter((j) => j.id !== id)), [setJsps]);

  const addJspYear = useCallback(
    (annee, { reconduire, conserverEquipement }) => {
      setJspYears((years) => (years.includes(annee) ? years : [...years, annee].sort()));
      if (reconduire) {
        setJsps((list) => {
          const source = list.filter((j) => j.annee === jspYear);
          const already = new Set(list.filter((j) => j.annee === annee).map((j) => `${j.nom}|${j.prenom}`));
          const reconduits = source
            .map((j) => ({ j, niveau: promoteNiveau(j.niveau) }))
            .filter(({ j, niveau }) => niveau && !already.has(`${j.nom}|${j.prenom}`))
            .map(({ j, niveau }) => {
              const base = conserverEquipement ? { ...j } : { ...newJsp(annee), nom: j.nom, prenom: j.prenom };
              return { ...base, id: uid(), annee, niveau };
            });
          return [...list, ...reconduits];
        });
      }
      setJspYear(annee);
    },
    [jspYear, setJspYears, setJsps, setJspYear],
  );

  /* ---------- Utilisateurs (administrateur) ---------- */

  const upsertUser = useCallback(
    async (user) => {
      const exists = users.some((u) => u.id === user.id);
      const body = {
        identifiant: user.identifiant,
        nom: user.nom,
        prenom: user.prenom,
        grade: user.grade,
        email: user.email,
        role: user.role,
        access: user.access,
        actif: user.actif,
        password: user.password || undefined,
      };
      const r = exists
        ? await apiRequest(`users/${encodeURIComponent(user.id)}`, { method: 'PUT', body })
        : await apiRequest('users', { method: 'POST', body });
      await refreshUsers();
      if (currentUserRef.current && r.user.id === currentUserRef.current.id) setSession((s) => ({ ...s, user: r.user }));
      return r.user;
    },
    [users, refreshUsers],
  );

  const deleteUser = useCallback(
    async (id) => {
      await apiRequest(`users/${encodeURIComponent(id)}`, { method: 'DELETE' });
      await refreshUsers();
    },
    [refreshUsers],
  );

  const setUserPassword = useCallback(
    async (id, password, mustChangePassword = true) => {
      await apiRequest(`users/${encodeURIComponent(id)}/password`, { method: 'POST', body: { password, mustChangePassword } });
      await refreshUsers();
    },
    [refreshUsers],
  );

  const value = {
    session,
    loadSession,
    dataStatus,
    refreshData,
    syncStatus,
    settings: settingsS.value,
    setSettings: settingsS.setValue,
    users,
    currentUser,
    login,
    logout,
    changeOwnPassword,
    setUserPassword,
    can,
    upsertUser,
    deleteUser,
    refreshUsers,
    needsSetup,
    completeSetup,
    legacyAvailable,
    importLegacy,
    exportServerData,
    importServerData,
    loadJournal,
    bilans,
    draftBilan,
    createBilan,
    updateBilan,
    deleteBilan,
    duplicateBilan,
    jsps,
    appels,
    setAppels: appelsS.setValue,
    jspYears: yearsS.value,
    jspYear,
    setJspYear,
    upsertJsp,
    updateJspField,
    deleteJsp,
    addJspYear,
    jspStock: stockS.value,
    setJspStock: stockS.setValue,
    notify,
    toasts,
    print,
    printNode,
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
