import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, Alert, AppState, type AppStateStatus, StyleSheet, Text, View } from "react-native";
import { StatusBar } from "expo-status-bar";
import * as Notifications from "expo-notifications";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator, type NativeStackScreenProps } from "@react-navigation/native-stack";
// SafeAreaView depuis safe-area-context (pas react-native) : le RN built-in
// n'applique pas les insets sur Android, donc la barre de navigation systeme
// bouffait les boutons de l'app en bas de l'ecran.
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import type { MobileUser } from "./src/types/auth";
import type { MobileIntervention } from "./src/types/intervention";
import { clearSession, readSession, saveSession } from "./src/services/tokenStorage";
import * as authApi from "./src/services/authApi";
import * as interventionsApi from "./src/services/interventionsApi";
import { clearAllPrecheckDrafts } from "./src/services/precheckDraftStorage";
import {
  clearOfflineCredential,
  saveOfflineCredential,
  verifyOfflineCredential,
} from "./src/services/offlineCredentialStore";
import {
  defineBackgroundNotificationTask,
  registerBackgroundNotificationTask,
} from "./src/services/backgroundSync";
import {
  clearApiAuthTokens,
  initializeApiClientAuth,
  isApiClientError,
  registerSessionRevokedHandler,
  setApiAuthTokens,
} from "./src/services/apiClient";
import {
  clearEnvironmentConfig,
  getEnvironmentBaseUrl,
  getInviteToken,
  saveInviteToken,
  setEnvironmentBaseUrl,
} from "./src/core/environmentService";
import {
  clearMobileConfig,
  getMobileConfigForBaseUrl,
  saveMobileConfig,
} from "./src/core/mobileConfigService";
import {
  configFromInstance,
  listInstances,
  removeInstance,
  upsertInstance,
  type SavedInstance,
} from "./src/core/instancesService";
import { fetchMobileConfig } from "./src/services/mobileConfigApi";
import type { MobileConfig } from "./src/types/mobileConfig";
import {
  clearLocalDatabase,
  getInterventionsSnapshot,
  getLocalDbOwnerId,
  getPendingQueueItems,
  initializeLocalDatabase,
  setLocalDbOwnerId,
} from "./src/core/localDatabase";
import {
  isNetworkOnline,
  registerBillingWallHandler,
  subscribeNetworkChanges,
  syncOnReconnect,
} from "./src/services/syncService";
import { configureNotificationHandler, syncPushRegistration } from "./src/services/pushNotifications";
import MissionsDashboard from "./src/features/missions/MissionsDashboard";
import MissionDetailScreen from "./src/features/missions/MissionDetailScreen";
import WorkflowInterventionScreen from "./src/features/missions/WorkflowInterventionScreen";
import HomeHub, { type HomeCard } from "./src/features/home/HomeHub";
import StatsScreen from "./src/features/home/StatsScreen";
import PlanningScreen from "./src/features/home/PlanningScreen";
import MyInfoScreen from "./src/features/home/MyInfoScreen";
import SupportScreen from "./src/features/home/SupportScreen";
import BottlesScreen from "./src/features/home/BottlesScreen";
import OnboardingScreen from "./src/features/onboarding/OnboardingScreen";
import LoginScreen from "./src/features/auth/LoginScreen";
import type { AppStackParamList, RootStackParamList } from "./src/navigation/types";
import { navigationRef, navigateToPlanning, type PlanningDay } from "./src/navigation/navigationRef";
import ErrorBoundary from "./src/shared/ErrorBoundary";
import UpdateGate from "./src/features/updates/UpdateGate";
import { isTodoStatus } from "./src/shared/interventionUtils";
import { installCrashHandlers } from "./src/core/crashHandlers";
import { setCrashContext } from "./src/core/crashLog";

// Capteurs globaux d'erreurs JS (fatales + rejets de promesse) installes au
// chargement du module, avant tout rendu, pour capter les crashs au plus tot.
// Voir OBSERVABILITE_CRASH.md (approche A).
installCrashHandlers();

const RootStack = createNativeStackNavigator<RootStackParamList>();
const AppStack = createNativeStackNavigator<AppStackParamList>();

// Badge du hub d'accueil : nombre d'interventions "a faire / en cours".
// On reutilise la source unique du vocabulaire de statuts (interventionUtils)
// au lieu de redupliquer le Set ici.
// Definit la tache de fond de pre-cache (couche 2) AU SCOPE MODULE : doit etre
// connue d'expo-task-manager au demarrage, avant le rendu React, pour pouvoir
// etre reveillee par un data push meme app fermee. L'enregistrement aupres de
// l'OS (registerBackgroundNotificationTask) se fait au boot.
defineBackgroundNotificationTask();

function countTodo(interventions: MobileIntervention[]): number {
  return interventions.filter((i) => isTodoStatus(i.statut)).length;
}

type MissionsScreenProps = NativeStackScreenProps<AppStackParamList, "Missions"> & {
  brandColor: string;
  interventions: MobileIntervention[];
  loadingInterventions: boolean;
  interventionsError: string | null;
  isOnline: boolean;
  syncingQueue: boolean;
  statusMessage: string | null;
  activeMissionsTab: "todo" | "done";
  setActiveMissionsTab: (tab: "todo" | "done") => void;
  onRefreshInterventions: () => void;
};

// Liste des interventions. N'est plus l'accueil : on y arrive depuis le hub via
// une carte. Le header natif (titre + bouton retour) remplace l'ancien header
// custom, d'ou le simple conteneur ici.
function MissionsScreen({
  navigation,
  brandColor,
  interventions,
  loadingInterventions,
  interventionsError,
  isOnline,
  syncingQueue,
  statusMessage,
  activeMissionsTab,
  setActiveMissionsTab,
  onRefreshInterventions,
}: Readonly<MissionsScreenProps>) {
  return (
    <View style={styles.subScreen}>
      <MissionsDashboard
        interventions={interventions}
        loadingInterventions={loadingInterventions}
        interventionsError={interventionsError}
        isOnline={isOnline}
        syncingQueue={syncingQueue}
        brandColor={brandColor}
        activeTab={activeMissionsTab}
        onChangeTab={setActiveMissionsTab}
        onRefresh={onRefreshInterventions}
        onOpenDetail={(id) => {
          navigation.navigate("MissionDetail", { interventionId: id, brandColor });
        }}
      />
      {statusMessage ? <Text style={styles.status}>{statusMessage}</Text> : null}
    </View>
  );
}

type AppScreenRenderers = {
  renderHome: (props: NativeStackScreenProps<AppStackParamList, "Home">) => React.ReactElement;
  renderMissions: (props: NativeStackScreenProps<AppStackParamList, "Missions">) => React.ReactElement;
  renderStats: (props: NativeStackScreenProps<AppStackParamList, "Stats">) => React.ReactElement;
  renderMyInfo: (props: NativeStackScreenProps<AppStackParamList, "MyInfo">) => React.ReactElement;
  renderSupport: (props: NativeStackScreenProps<AppStackParamList, "Support">) => React.ReactElement;
  renderBottles: (props: NativeStackScreenProps<AppStackParamList, "Bottles">) => React.ReactElement;
  renderPlanning: (props: NativeStackScreenProps<AppStackParamList, "Planning">) => React.ReactElement;
};

function AppNavigatorStack({
  renderHome,
  renderMissions,
  renderStats,
  renderMyInfo,
  renderSupport,
  renderBottles,
  renderPlanning,
}: Readonly<AppScreenRenderers>) {
  return (
    <AppStack.Navigator>
      <AppStack.Screen name="Home" options={{ headerShown: false }}>
        {renderHome}
      </AppStack.Screen>
      <AppStack.Screen name="Missions" options={{ title: "Mes interventions" }}>
        {renderMissions}
      </AppStack.Screen>
      <AppStack.Screen name="Stats" options={{ title: "Statistiques" }}>
        {renderStats}
      </AppStack.Screen>
      <AppStack.Screen name="MyInfo" options={{ title: "Mes informations" }}>
        {renderMyInfo}
      </AppStack.Screen>
      <AppStack.Screen name="Support" options={{ title: "Support" }}>
        {renderSupport}
      </AppStack.Screen>
      <AppStack.Screen name="Bottles" options={{ title: "Mes bouteilles de gaz" }}>
        {renderBottles}
      </AppStack.Screen>
      <AppStack.Screen name="Planning" options={{ title: "Calendrier d'intervention" }}>
        {renderPlanning}
      </AppStack.Screen>
      <AppStack.Screen
        name="MissionDetail"
        component={MissionDetailScreen}
        options={{ title: "Detail mission" }}
      />
      <AppStack.Screen
        name="WorkflowIntervention"
        component={WorkflowInterventionScreen}
        options={{ title: "Workflow intervention" }}
      />
    </AppStack.Navigator>
  );
}


type OnboardingPayload = {
  url: string | null;
  inviteToken: string | null;
  email: string | null;
};

function parseOnboardingPayload(raw: string): OnboardingPayload {
  const trimmed = String(raw || "").trim();
  const empty: OnboardingPayload = { url: null, inviteToken: null, email: null };
  if (!trimmed) {
    return empty;
  }

  const pickString = (obj: Record<string, unknown>, keys: string[]): string | null => {
    for (const key of keys) {
      const v = obj[key];
      if (typeof v === "string") return v;
    }
    return null;
  };

  try {
    const parsedJson = JSON.parse(trimmed) as Record<string, unknown>;
    return {
      url: pickString(parsedJson, ["url", "base_url", "instance_url"]),
      inviteToken: pickString(parsedJson, ["invite_token", "token"]),
      email: pickString(parsedJson, ["email"]),
    };
  } catch {
    // Continue with URL parsing.
  }

  try {
    const urlObj = new URL(trimmed);
    const queryUrl =
      urlObj.searchParams.get("url") ||
      urlObj.searchParams.get("base_url") ||
      urlObj.searchParams.get("instance_url");

    return {
      url: queryUrl || trimmed,
      inviteToken: urlObj.searchParams.get("invite_token") || urlObj.searchParams.get("token"),
      email: urlObj.searchParams.get("email"),
    };
  } catch {
    return empty;
  }
}

// Configure une seule fois la presentation des notifications au premier plan.
configureNotificationHandler();

// Reconnexion HORS-LIGNE (point D) : verifie le credential Argon2id en cache et
// lit la session SecureStore. Renvoie les donnees a appliquer, ou null si le
// credential n'est pas reconnu. Sorti de onLogin pour aplatir son catch.
async function resolveOfflineLogin(
  baseUrl: string,
  email: string,
  password: string
): Promise<{
  user: MobileUser;
  accessToken: string | null;
  refreshToken: string | null;
  hasTokens: boolean;
} | null> {
  const cachedUser = await verifyOfflineCredential(baseUrl, email, password);
  if (!cachedUser) {
    return null;
  }
  const session = await readSession();
  const hasTokens = Boolean(session.accessToken && session.refreshToken);
  return {
    user: cachedUser,
    accessToken: session.accessToken ?? null,
    refreshToken: session.refreshToken ?? null,
    hasTokens,
  };
}

// Memorise le credential pour la reconnexion hors-ligne (hash Argon2id en
// SecureStore). Best-effort : ne doit jamais bloquer ni casser le login.
async function persistOfflineCredentialSafe(
  baseUrl: string,
  email: string,
  password: string,
  user: MobileUser
): Promise<void> {
  try {
    await saveOfflineCredential(baseUrl, email, password, user);
  } catch {
    // best-effort : pas de reconnexion offline si l'enregistrement echoue.
  }
}

// Garde anti-fuite inter-comptes : si le cache local appartient a un AUTRE
// technicien (device partage), on le purge avant de charger les interventions
// du nouvel utilisateur. Meme compte qui se reconnecte -> on NE purge PAS (sa
// file de sync hors-ligne survit).
async function reconcileLocalDbOwner(newUserId: number): Promise<void> {
  const previousOwner = await getLocalDbOwnerId();
  if (previousOwner !== null && previousOwner !== newUserId) {
    await clearLocalDatabase();
    await clearAllPrecheckDrafts();
  }
  await setLocalDbOwnerId(newUserId);
}

// Rafraichit le branding (logo, couleur, nom, support) en fond et persiste pour
// le prochain cold start. Best-effort : ne bloque pas le boot, ignore les
// erreurs (hors-ligne -> on garde le cache). `onFresh` applique la config.
async function refreshBrandingInBackground(
  baseUrl: string,
  onFresh: (config: MobileConfig) => void
): Promise<void> {
  try {
    const freshConfig = await fetchMobileConfig(baseUrl);
    await saveMobileConfig(baseUrl, freshConfig);
    onFresh(freshConfig);
  } catch {
    // Hors-ligne ou endpoint indispo : on conserve la config en cache.
  }
}

function AppInner() {
  const [booting, setBooting] = useState(true);
  const [isOnline, setIsOnline] = useState(true);
  const [syncingQueue, setSyncingQueue] = useState(false);
  const [validatingInstance, setValidatingInstance] = useState(false);
  const [submitting, setSubmitting] = useState(false);

  const [instanceUrl, setInstanceUrl] = useState("");
  const [configuredBaseUrl, setConfiguredBaseUrl] = useState<string | null>(null);
  const [mobileConfig, setMobileConfig] = useState<MobileConfig | null>(null);
  const [inviteToken, setInviteToken] = useState<string | null>(null);
  // Instances enregistrees (multi-entreprises) : permet de rebasculer sans
  // re-scanner le QR ni retaper l'email.
  const [savedInstances, setSavedInstances] = useState<SavedInstance[]>([]);

  const [onboardingError, setOnboardingError] = useState<string | null>(null);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [statusMessage, setStatusMessage] = useState<string | null>(null);

  const [user, setUser] = useState<MobileUser | null>(null);
  const [interventions, setInterventions] = useState<MobileIntervention[]>([]);
  const [loadingInterventions, setLoadingInterventions] = useState(false);
  const [interventionsError, setInterventionsError] = useState<string | null>(null);
  // Tab courante remontee ici pour survivre aux remounts de MissionsDashboard
  // declenches par les re-renders d'App (DashboardRoute est redefini a
  // chaque render, donc l'enfant MissionsDashboard est remonte et perd
  // son state local — d'ou le "ça refresh et reste sur À faire" sans
  // cette lift-up).
  const [activeMissionsTab, setActiveMissionsTab] = useState<"todo" | "done">("todo");

  const appDisplayName = mobileConfig?.app_name || "MissioFlow Mobile";
  const brandColor = mobileConfig?.primary_color || "#1e56a8";
  const logoUrl = mobileConfig?.logo_url || null;

  // Situe les incidents (instance + tenant) sans donnee sensible, pour le log de
  // crash. Voir OBSERVABILITE_CRASH.md (approche A, §6).
  useEffect(() => {
    setCrashContext({
      instanceUrl: configuredBaseUrl,
      tenantId: mobileConfig?.tenant_id ?? null,
    });
  }, [configuredBaseUrl, mobileConfig?.tenant_id]);

  // Session evincee (Lot 5, verrouillage per-seat) : quand le compte se
  // reconnecte ailleurs (web ou autre appareil), le backend renvoie un 401
  // code=SESSION_REVOKED. L'apiClient a deja purge les tokens ; ici on ramene
  // l'utilisateur a l'ecran de login avec un message dedie. Mount-once : les
  // setters de useState ont une identite stable, pas besoin de les lister.
  useEffect(() => {
    registerSessionRevokedHandler(() => {
      setUser(null);
      setInterventions([]);
      setPassword("");
      setStatusMessage(null);
      setError("Vous avez ete deconnecte car votre compte a ete utilise sur un autre appareil.");
    });
    return () => registerSessionRevokedHandler(null);
  }, []);

  // Mur de facturation (cote serveur, Lot 1) : une ecriture de completion
  // synchronisee en fond est refusee (402 BILLING_WALL) quand l'abonnement du
  // tenant n'est plus actif. La sync garde l'item en file (re-pousse a la
  // reactivation) ; ici on informe juste le tech, SANS logout (le mur n'est pas
  // une session invalide). Une seule alerte par episode (cf. billingWallNotified
  // cote syncService). Mount-once.
  useEffect(() => {
    registerBillingWallHandler((message) => {
      Alert.alert(
        "Abonnement requis",
        message ||
          "Abonnement requis. Seules les interventions deja en cours du jour peuvent etre finalisees. Vos saisies sont conservees et seront envoyees des la reactivation.",
        [{ text: "Compris" }]
      );
    });
    return () => registerBillingWallHandler(null);
  }, []);

  const loadInterventions = async (): Promise<number> => {
    setLoadingInterventions(true);
    setInterventionsError(null);

    try {
      // Fenetre LARGE volontaire : le backend mobile defaut a -7/+7 jours, ce
      // qui masquait au tech ses interventions futures (mois prochain absent du
      // calendrier/des stats). Objectif produit : le tech voit 100% de ses
      // interventions planifiees. On couvre l'annee passee -> +2 ans.
      const nowYear = new Date().getFullYear();
      const payload = await interventionsApi.getMyInterventions({
        date_debut: `${nowYear - 1}-01-01`,
        date_fin: `${nowYear + 2}-12-31`,
      });
      setInterventions(payload.items);
      return payload.count;
    } catch (e) {
      const message = e instanceof Error ? e.message : "Chargement des interventions impossible";
      setInterventionsError(message);
      return 0;
    } finally {
      setLoadingInterventions(false);
    }
  };

  // Precharge les workflows (etapes) des interventions "a faire" pour le mode
  // hors-ligne. Les etapes ne sont PAS dans la liste et sont materialisees
  // paresseusement cote serveur a la 1ere lecture : sans ce prechargement, un
  // tech qui ouvre une intervention pour la 1ere fois hors-ligne (ex: descend a
  // la cave sans l'avoir ouverte en ligne) voit "aucune etape". Best-effort, en
  // tache de fond : ne bloque jamais l'UI. A appeler EN LIGNE (login / boot).
  const prefetchTodoWorkflows = async () => {
    try {
      const snap = await getInterventionsSnapshot();
      const ids = snap.items.filter((i) => isTodoStatus(i.statut)).map((i) => i.id);
      if (ids.length === 0) {
        return;
      }
      const n = await interventionsApi.prefetchWorkflows(ids);
      if (n > 0) {
        setStatusMessage(`${n} intervention(s) prêtes hors-ligne.`);
      }
    } catch {
      // best-effort : pas de prechargement si offline / erreur.
    }
  };

  // Refs "derniere valeur" pour les listeners de notification (effet mount-once)
  // sans recreer les abonnements a chaque render.
  const loadInterventionsRef = useRef(loadInterventions);
  loadInterventionsRef.current = loadInterventions;
  const userRef = useRef<MobileUser | null>(user);
  userRef.current = user;
  // Vrai quand la session courante a ete restauree HORS-LIGNE sans token valide
  // (reconnexion offline apres deconnexion). Au retour du reseau, on invite a se
  // reconnecter pour synchroniser (le travail en file est preserve).
  const offlineTokenlessRef = useRef(false);
  // Deep-link calendrier en attente : si la notif "resume quotidien" est tapee
  // avant que l'app soit prete (cold start, session pas encore restauree), on
  // memorise le jour cible ici et on rejoue des que `user` est charge.
  const pendingPlanningDayRef = useRef<PlanningDay | null>(null);

  // Une notification push signifie qu'un admin a modifie une mission. Si le
  // technicien est connecte, on recharge la liste pour refleter le changement
  // immediatement (reception au premier plan + tap sur la notification).
  useEffect(() => {
    const reloadIfLogged = () => {
      if (userRef.current) {
        loadInterventionsRef.current().catch(() => {});
      }
    };

    // Tap sur une notification : on recharge la liste, et si c'est le resume
    // quotidien (data.type === "daily_summary") on ouvre le calendrier sur le
    // bon jour. Si l'app n'est pas encore prete (session non restauree), on
    // memorise la cible pour la rejouer (cf. effet ci-dessous).
    const onTap = (response: Notifications.NotificationResponse) => {
      reloadIfLogged();
      const data = response.notification.request.content.data as
        | { type?: string; day?: string }
        | undefined;
      if (data?.type !== "daily_summary") {
        return;
      }
      const day: PlanningDay = data.day === "tomorrow" ? "tomorrow" : "today";
      if (!(userRef.current && navigateToPlanning(day))) {
        pendingPlanningDayRef.current = day;
      }
    };

    const receivedSub = Notifications.addNotificationReceivedListener(reloadIfLogged);
    const responseSub = Notifications.addNotificationResponseReceivedListener(onTap);
    return () => {
      receivedSub.remove();
      responseSub.remove();
    };
  }, []);

  // Rejoue un deep-link calendrier en attente une fois la session restauree :
  // l'ecran "AppNavigator" (qui contient "Planning") n'est monte qu'avec un
  // user, donc la navigation au moment du tap pouvait echouer.
  useEffect(() => {
    if (!user || !pendingPlanningDayRef.current) {
      return;
    }
    const day = pendingPlanningDayRef.current;
    // setTimeout(0) : laisse l'AppNavigator se monter avant de naviguer.
    const timer = setTimeout(() => {
      if (navigateToPlanning(day)) {
        pendingPlanningDayRef.current = null;
      }
    }, 0);
    return () => clearTimeout(timer);
  }, [user]);

  const applyEnvironmentFromValue = async (value: string): Promise<boolean> => {
    setOnboardingError(null);
    setValidatingInstance(true);

    try {
      const normalized = await setEnvironmentBaseUrl(value);
      const remoteConfig = await fetchMobileConfig(normalized);
      await saveMobileConfig(normalized, remoteConfig);

      // Memorise l'instance dans le registre local (multi-entreprises).
      const updatedInstances = await upsertInstance({
        base_url: normalized,
        app_name: remoteConfig.app_name,
        primary_color: remoteConfig.primary_color,
        logo_url: remoteConfig.logo_url,
        tenant_id: remoteConfig.tenant_id,
        support_email: remoteConfig.support_email,
        support_phone: remoteConfig.support_phone,
        last_email: email.trim() || null,
        saved_at: new Date().toISOString(),
      });
      setSavedInstances(updatedInstances);

      setConfiguredBaseUrl(normalized);
      setMobileConfig(remoteConfig);
      setInstanceUrl(normalized);
      setStatusMessage("Instance configuree.");
      return true;
    } catch (e) {
      const message = e instanceof Error ? e.message : "Instance inaccessible ou invalide";
      await clearEnvironmentConfig();
      await clearMobileConfig();
      setOnboardingError(message);
      setConfiguredBaseUrl(null);
      setMobileConfig(null);
      return false;
    } finally {
      setValidatingInstance(false);
    }
  };

  const onSaveEnvironment = async () => {
    await saveInviteToken(null);
    setInviteToken(null);
    await applyEnvironmentFromValue(instanceUrl);
  };

  const onScanPayload = async (raw: string) => {
    const parsed = parseOnboardingPayload(raw);
    if (!parsed.url) {
      setOnboardingError("QR invalide: URL d'instance introuvable.");
      return;
    }

    if (parsed.email) {
      setEmail(parsed.email);
    }

    if (parsed.inviteToken) {
      setInviteToken(parsed.inviteToken);
    }

    const valid = await applyEnvironmentFromValue(parsed.url);
    if (valid && parsed.inviteToken) {
      await saveInviteToken(parsed.inviteToken);
    }
  };

  // Bascule vers une instance deja enregistree : pas de QR ni d'URL a retaper,
  // email pre-rempli. On arrive sur l'ecran de connexion de cette instance.
  const onSelectSavedInstance = async (entry: SavedInstance) => {
    setOnboardingError(null);
    setValidatingInstance(true);
    try {
      const normalized = await setEnvironmentBaseUrl(entry.base_url);
      // Re-fetch best-effort de la config (capte un changement de nom/branding),
      // fallback sur la config enregistree si hors-ligne.
      let config = configFromInstance(entry);
      try {
        config = await fetchMobileConfig(normalized);
        const refreshed = await upsertInstance({
          base_url: normalized,
          app_name: config.app_name,
          primary_color: config.primary_color,
          logo_url: config.logo_url,
          tenant_id: config.tenant_id,
          support_email: config.support_email,
          support_phone: config.support_phone,
          saved_at: new Date().toISOString(),
        });
        setSavedInstances(refreshed);
      } catch {
        // hors-ligne : on garde la config du registre.
      }
      await saveMobileConfig(normalized, config);
      setConfiguredBaseUrl(normalized);
      setMobileConfig(config);
      setInstanceUrl(normalized);
      setEmail(entry.last_email ?? "");
      setPassword("");
    } catch (e) {
      setOnboardingError(e instanceof Error ? e.message : "Instance inaccessible");
    } finally {
      setValidatingInstance(false);
    }
  };

  const onRemoveSavedInstance = async (baseUrl: string) => {
    const updated = await removeInstance(baseUrl);
    setSavedInstances(updated);
  };

  const onChangeInstance = async () => {
    // Garde-fou : changer d'instance efface les donnees locales de l'instance
    // courante (y compris la file d'actions hors-ligne non synchronisees). On
    // previent l'utilisateur si du travail risque d'etre perdu / s'il est
    // hors-ligne (il ne pourra pas se reconnecter ailleurs sans reseau).
    let pendingCount = 0;
    try {
      const pending = await getPendingQueueItems();
      pendingCount = pending.length;
    } catch {
      // best-effort : si on ne peut pas lire la file, on avertit quand meme.
    }

    const lines = [
      "Cette action efface les donnees locales de cette instance et revient a la liste des instances.",
    ];
    if (pendingCount > 0) {
      lines.push(
        `\n⚠️ ${pendingCount} action(s) non synchronisee(s) seront definitivement perdues.`
      );
    }
    if (!isOnline) {
      lines.push(
        "\n⚠️ Tu es hors-ligne : tu ne pourras pas te reconnecter a une autre instance sans reseau."
      );
    }

    Alert.alert(
      "Changer d'instance",
      lines.join("\n"),
      [
        { text: "Annuler", style: "cancel" },
        {
          text: "Changer quand meme",
          style: "destructive",
          onPress: () => {
            (async () => {
              await clearSession();
              clearApiAuthTokens();
              await clearAllPrecheckDrafts();
              await clearOfflineCredential();
              await clearLocalDatabase();
              await clearMobileConfig();
              await clearEnvironmentConfig();
              setConfiguredBaseUrl(null);
              setMobileConfig(null);
              setInstanceUrl("");
              setInviteToken(null);
              setUser(null);
              setEmail("");
              setPassword("");
              setStatusMessage(null);
              setError(null);
              setOnboardingError(null);
            })();
          },
        },
      ]
    );
  };

  const onLogin = async () => {
    if (!email.trim() || !password) {
      setError("Email et mot de passe sont requis.");
      return;
    }

    setSubmitting(true);
    setError(null);

    try {
      const result = await authApi.login(email.trim(), password, inviteToken);
      await saveSession(result.access_token, result.refresh_token, result.user);
      // Notifier l'apiClient du nouveau token : sans ca, le cache memoire
      // reste a null (car hydrated=true apres le boot initial) et les
      // requetes suivantes partent sans header Authorization.
      setApiAuthTokens(result.access_token, result.refresh_token);
      offlineTokenlessRef.current = false;

      // Memorise le credential pour la reconnexion HORS-LIGNE (point D).
      if (configuredBaseUrl) {
        await persistOfflineCredentialSafe(configuredBaseUrl, email.trim(), password, result.user);
      }

      // Anti-fuite inter-comptes : purge le cache local s'il appartient a un
      // autre technicien (cf. reconcileLocalDbOwner).
      await reconcileLocalDbOwner(result.user.id);

      await saveInviteToken(null);
      setInviteToken(null);
      setUser(result.user);
      setPassword("");

      // Memorise l'email pour cette instance (plus a retaper la prochaine fois).
      if (configuredBaseUrl) {
        const updatedInstances = await upsertInstance({
          base_url: configuredBaseUrl,
          last_email: email.trim(),
          saved_at: new Date().toISOString(),
        });
        setSavedInstances(updatedInstances);
      }

      const count = await loadInterventions();
      setStatusMessage(`Connecte. ${count} intervention(s) chargee(s).`);
      // Enregistrement push best-effort : ne doit jamais bloquer le login.
      void syncPushRegistration();
      // Precharge les workflows pour le hors-ligne (tache de fond).
      void prefetchTodoWorkflows();
    } catch (e) {
      // Echec du login en ligne. Si c'est une erreur RESEAU (pas une vraie 401
      // serveur), on tente la reconnexion HORS-LIGNE (point D) via le credential
      // cache. Une 401 reelle (mauvais identifiants) ne declenche PAS l'offline.
      const isNetworkErr = isApiClientError(e) && e.status === undefined;
      const offline =
        isNetworkErr && configuredBaseUrl
          ? await resolveOfflineLogin(configuredBaseUrl, email.trim(), password)
          : null;

      if (offline) {
        setApiAuthTokens(offline.accessToken, offline.refreshToken);
        // Sans tokens (apres deconnexion), la session est offline-only : on
        // marque pour inviter a se reconnecter au retour du reseau.
        offlineTokenlessRef.current = !offline.hasTokens;
        await setLocalDbOwnerId(offline.user.id);
        setUser(offline.user);
        setPassword("");
        await loadInterventions();
        setStatusMessage(
          offline.hasTokens
            ? "Connecte hors-ligne. Donnees en cache disponibles."
            : "Connecte hors-ligne. Reconnecte-toi au retour du reseau pour synchroniser."
        );
      } else if (isNetworkErr && configuredBaseUrl) {
        // Reseau coupe mais credential non reconnu / pas de session en cache.
        setError(
          "Connexion impossible hors-ligne : identifiants non reconnus pour cette instance, ou aucune session en cache. Connecte-toi une fois en ligne d'abord."
        );
      } else {
        const message = e instanceof Error ? e.message : "Erreur de connexion";
        setError(message);
      }
    } finally {
      setSubmitting(false);
    }
  };

  const onLogout = async () => {
    try {
      const session = await readSession();
      if (session.refreshToken) {
        await authApi.logout(session.refreshToken);
      }
    } catch {
      // ignore
    }

    await clearSession();
    clearApiAuthTokens();
    setUser(null);
    setInterventions([]);
    setStatusMessage("Deconnexion effectuee.");
  };

  const onRefreshInterventions = async () => {
    const count = await loadInterventions();
    setStatusMessage(`Interventions rafraichies (${count}).`);
  };

  // Recharge le profil (apres edition telephone/mot de passe) pour refleter les
  // champs enrichis renvoyes par /mobile/me.php.
  const refreshUser = async () => {
    try {
      const current = await authApi.me();
      setUser(current);
    } catch {
      // best-effort : on garde l'utilisateur courant si le refresh echoue.
    }
  };

  useEffect(() => {
    let isMounted = true;
    let unsubscribeNetwork: (() => void) | null = null;
    let appStateSub: { remove: () => void } | null = null;

    // Synchronisation incrementale : pousse la file hors-ligne puis tire le
    // delta serveur (sync_pull.php) qui fusionne le cache local. On rafraichit
    // ENSUITE l'affichage depuis le snapshot fusionne — toujours, et non plus
    // conditionne a pushed||pulled (avant : un retour online sans modif locale
    // ne rafraichissait jamais -> donnees perimees jusqu'au cold start).
    const triggerReconnectSync = async () => {
      // Pas d'utilisateur, ou session offline-only sans token (rien a pousser
      // avec un token revoque) -> on ne tente pas de sync.
      if (!userRef.current || offlineTokenlessRef.current) {
        return;
      }
      setSyncingQueue(true);
      try {
        const result = await syncOnReconnect();

        const snapshot = await getInterventionsSnapshot();
        if (isMounted) {
          setInterventions(snapshot.items);
        }
        // M4 : une action en echec definitif (finalisation ou photo jamais passee)
        // prime sur le message de succes : la sync etant invisible, le technicien
        // doit etre averti explicitement plutot que de croire que tout est parti.
        if (result.dead > 0) {
          setStatusMessage(
            `⚠️ ${result.dead} action(s) n'ont pas pu être synchronisées. Contactez le support si le problème persiste.`
          );
        } else if (result.pushed > 0) {
          setStatusMessage(`${result.pushed} action(s) hors-ligne synchronisée(s).`);
        }
      } finally {
        // Garde-fou (#15) : un throw (ex. snapshot/sync) ne doit jamais laisser
        // l'indicateur de synchro bloque a true.
        if (isMounted) {
          setSyncingQueue(false);
        }
      }
    };

    const handleNetworkChange = (onlineState: boolean) => {
      if (!isMounted) {
        return;
      }
      setIsOnline(onlineState);
      if (!onlineState) {
        return;
      }
      // Session restauree hors-ligne sans token : au retour du reseau on ne peut
      // pas synchroniser (tokens revoques a la deconnexion). On invite a se
      // reconnecter ; le travail en file (SQLite) est preserve et repartira
      // apres le re-login.
      if (offlineTokenlessRef.current) {
        offlineTokenlessRef.current = false;
        Alert.alert(
          "De retour en ligne",
          "Reconnecte-toi pour synchroniser ton travail effectue hors-ligne.",
          [{ text: "OK", onPress: () => setUser(null) }]
        );
        return;
      }
      void triggerReconnectSync();
    };

    // Retour au premier plan = "redemarrage" pour une app en arriere-plan :
    // on resynchronise (delta) pour rattraper les modifs serveur survenues
    // pendant la veille (push manquee, longue mise en arriere-plan).
    const handleAppStateChange = async (state: AppStateStatus) => {
      if (state !== "active" || !userRef.current) {
        return;
      }
      const online = await isNetworkOnline();
      if (isMounted && online) {
        setIsOnline(true);
        void triggerReconnectSync();
      }
    };

    const bootstrap = async () => {
      try {
        await initializeLocalDatabase();
        // Enregistre la tache de fond de pre-cache (couche 2). Best-effort,
        // independant de la session : la tache gere sa propre auth au reveil.
        void registerBackgroundNotificationTask();

        const online = await isNetworkOnline();
        if (isMounted) {
          setIsOnline(online);
        }

        unsubscribeNetwork = subscribeNetworkChanges(handleNetworkChange);
        appStateSub = AppState.addEventListener("change", handleAppStateChange);

        await initializeApiClientAuth();

        const configuredUrl = await getEnvironmentBaseUrl();
        const storedInviteToken = await getInviteToken();
        setInviteToken(storedInviteToken);

        const instances = await listInstances();
        if (isMounted) {
          setSavedInstances(instances);
        }

        if (!configuredUrl) {
          setBooting(false);
          return;
        }

        setInstanceUrl(configuredUrl);
        // Pre-remplit l'email memorise pour l'instance active.
        const activeEntry = instances.find((i) => i.base_url === configuredUrl);
        if (activeEntry?.last_email && isMounted) {
          setEmail(activeEntry.last_email);
        }

        const cachedConfig = await getMobileConfigForBaseUrl(configuredUrl);
        if (!cachedConfig) {
          setBooting(false);
          return;
        }

        setConfiguredBaseUrl(configuredUrl);
        setMobileConfig(cachedConfig.config);

        // Rafraichit le branding (logo, couleur, nom, support) en fond : le
        // mobileConfig est cache localement, donc un changement cote tenant
        // (ex. nouveau logo_url) n'arrivait jamais a un user deja onboarde sans
        // qu'il re-selectionne son instance. Cf. refreshBrandingInBackground.
        void refreshBrandingInBackground(configuredUrl, (freshConfig) => {
          if (isMounted) {
            setMobileConfig(freshConfig);
          }
        });

        // Sème l'instance active dans le registre si elle n'y est pas encore.
        // Indispensable pour les utilisateurs deja connectes AVANT l'arrivee de
        // cette fonctionnalite : leur instance courante apparait aussitot dans
        // "Mes instances" sans avoir a re-scanner le QR / retaper le lien.
        const seededInstances = await upsertInstance({
          base_url: configuredUrl,
          app_name: cachedConfig.config.app_name,
          primary_color: cachedConfig.config.primary_color,
          logo_url: cachedConfig.config.logo_url,
          tenant_id: cachedConfig.config.tenant_id,
          support_email: cachedConfig.config.support_email,
          support_phone: cachedConfig.config.support_phone,
          last_email: activeEntry?.last_email ?? null,
          saved_at: new Date().toISOString(),
        });
        if (isMounted) {
          setSavedInstances(seededInstances);
        }

        const session = await readSession();
        if (!session.accessToken || !session.refreshToken) {
          setBooting(false);
          return;
        }

        try {
          const currentUser = await authApi.me();
          setUser(currentUser);
          await loadInterventions();
          // Session restauree : re-enregistrer le token push (best-effort).
          void syncPushRegistration();
          // Precharge les workflows pour le hors-ligne (tache de fond).
          void prefetchTodoWorkflows();
          // M3 : DRAINER LA FILE D'ECRITURE AU BOOT. Sans ca, un tech qui rouvre
          // l'app le matin (deja en ligne) avec du travail hors-ligne de la veille
          // ne le voyait pas repartir : addNetworkStateListener n'emet pas l'etat
          // initial et AppState ne refire pas au mount, donc aucun des deux
          // declencheurs de drain ne survenait avant un changement reseau/foreground.
          // On pose userRef.current ici (setUser n'est applique qu'au prochain
          // render) pour que le garde de triggerReconnectSync passe.
          if (online) {
            userRef.current = currentUser;
            offlineTokenlessRef.current = false;
            void triggerReconnectSync();
          }
        } catch (meError) {
          // Distinction cruciale pour l'usage terrain hors-ligne : une erreur
          // RESEAU (serveur injoignable / timeout) leve une ApiClientError SANS
          // status, alors qu'une vraie 401 (token revoque/invalide) porte
          // status=401 (et la session est deja purgee par le flux refresh /
          // SESSION_REVOKED). Sans ce garde-fou, relancer l'app sans reseau
          // appelait me() -> echec -> clearSession -> retour login : l'app
          // etait 100% en ligne.
          const isOffline = isApiClientError(meError) && meError.status === undefined;
          if (isOffline && session.user) {
            // Session optimiste : on ouvre l'app sur les donnees en cache
            // (snapshot SQLite). Les tokens restent valides cote SecureStore ;
            // la prochaine requete EN LIGNE re-validera, et un 401 reel
            // renverra alors au login via le flux normal.
            setApiAuthTokens(session.accessToken, session.refreshToken);
            setUser(session.user);
            await loadInterventions(); // best-effort: lit le snapshot local
            void syncPushRegistration();
          } else {
            await clearSession();
            clearApiAuthTokens();
          }
        }
      } catch {
        await clearSession();
        clearApiAuthTokens();
      }

      if (isMounted) {
        setBooting(false);
      }
    };

    bootstrap();

    return () => {
      isMounted = false;
      if (unsubscribeNetwork) {
        unsubscribeNetwork();
      }
      if (appStateSub) {
        appStateSub.remove();
      }
    };
  }, []);

  // On passe un render prop 'children' (et non 'component') a AppStack.Screen :
  // avec 'component={X}', React Navigation remonte l'ecran a chaque re-render
  // d'App -> useFocusEffect refire a l'infini. 'children' accepte un callback
  // stable sans declencher de remount.
  const renderHome = (
    screenProps: NativeStackScreenProps<AppStackParamList, "Home">
  ) => {
    const { navigation } = screenProps;
    // Ordre = disposition de la grille 2 colonnes :
    //   Interventions | Bouteilles
    //   Calendrier    | Mes informations
    //   Statistiques  | Support
    const cards: HomeCard[] = [
      {
        key: "missions",
        icon: "📋",
        title: "Mes interventions",
        subtitle: "A faire et terminees",
        badge: countTodo(interventions),
        onPress: () => navigation.navigate("Missions"),
      },
      {
        key: "bottles",
        icon: "🛢️",
        title: "Mes bouteilles de gaz",
        subtitle: "Bouteilles attribuees",
        onPress: () => navigation.navigate("Bottles"),
      },
      {
        key: "calendar",
        icon: "📅",
        title: "Calendrier d'intervention",
        subtitle: "Planning a venir",
        onPress: () => navigation.navigate("Planning"),
      },
      {
        key: "info",
        icon: "👨‍🔧",
        title: "Mes informations",
        subtitle: "Profil et instance",
        onPress: () => navigation.navigate("MyInfo"),
      },
      {
        key: "stats",
        icon: "📊",
        title: "Statistiques",
        subtitle: "Mon activite",
        onPress: () => navigation.navigate("Stats"),
      },
      {
        key: "support",
        icon: "💬",
        title: "Support",
        subtitle: "Aide et a propos",
        onPress: () => navigation.navigate("Support"),
      },
    ];
    return (
      <HomeHub
        appDisplayName={appDisplayName}
        brandColor={brandColor}
        logoUrl={logoUrl}
        userName={user?.name ?? null}
        isOnline={isOnline}
        cards={cards}
        onChangeInstance={onChangeInstance}
        onLogout={onLogout}
      />
    );
  };

  const renderMissions = (
    screenProps: NativeStackScreenProps<AppStackParamList, "Missions">
  ) => (
    <MissionsScreen
      {...screenProps}
      brandColor={brandColor}
      interventions={interventions}
      loadingInterventions={loadingInterventions}
      interventionsError={interventionsError}
      isOnline={isOnline}
      syncingQueue={syncingQueue}
      statusMessage={statusMessage}
      activeMissionsTab={activeMissionsTab}
      setActiveMissionsTab={setActiveMissionsTab}
      onRefreshInterventions={onRefreshInterventions}
    />
  );

  const renderStats = (
    _screenProps: NativeStackScreenProps<AppStackParamList, "Stats">
  ) => <StatsScreen interventions={interventions} brandColor={brandColor} />;

  const renderMyInfo = (
    _screenProps: NativeStackScreenProps<AppStackParamList, "MyInfo">
  ) => (
    <MyInfoScreen
      user={user}
      instanceUrl={configuredBaseUrl}
      brandColor={brandColor}
      onUserRefreshed={refreshUser}
    />
  );

  const renderSupport = (
    _screenProps: NativeStackScreenProps<AppStackParamList, "Support">
  ) => (
    <SupportScreen
      appDisplayName={appDisplayName}
      instanceUrl={configuredBaseUrl}
      tenantId={mobileConfig?.tenant_id ?? null}
      supportEmail={mobileConfig?.support_email ?? null}
      supportPhone={mobileConfig?.support_phone ?? null}
    />
  );

  const renderBottles = (
    _screenProps: NativeStackScreenProps<AppStackParamList, "Bottles">
  ) => <BottlesScreen brandColor={brandColor} />;

  const renderPlanning = (
    screenProps: NativeStackScreenProps<AppStackParamList, "Planning">
  ) => (
    <PlanningScreen
      interventions={interventions}
      brandColor={brandColor}
      initialDay={screenProps.route?.params?.initialDay}
      onOpenDetail={(id) =>
        screenProps.navigation.navigate("MissionDetail", { interventionId: id, brandColor })
      }
    />
  );

  const renderAppNavigator = () => (
    <AppNavigatorStack
      renderHome={renderHome}
      renderMissions={renderMissions}
      renderStats={renderStats}
      renderMyInfo={renderMyInfo}
      renderSupport={renderSupport}
      renderBottles={renderBottles}
      renderPlanning={renderPlanning}
    />
  );

  if (booting) {
    return (
      <SafeAreaProvider>
        <SafeAreaView style={styles.centered}>
          <StatusBar style="dark" />
          <ActivityIndicator size="large" />
          <Text style={styles.muted}>Chargement...</Text>
        </SafeAreaView>
      </SafeAreaProvider>
    );
  }

  const renderOnboardingScreen = () => (
    <RootStack.Screen name="Onboarding">
      {() => (
        <OnboardingScreen
          appDisplayName={appDisplayName}
          brandColor={brandColor}
          logoUrl={logoUrl}
          instanceUrl={instanceUrl}
          onboardingError={onboardingError}
          validatingInstance={validatingInstance}
          onChangeInstanceUrl={setInstanceUrl}
          onSaveEnvironment={() => {
            onSaveEnvironment();
          }}
          onScanPayload={onScanPayload}
          savedInstances={savedInstances}
          onSelectInstance={onSelectSavedInstance}
          onRemoveInstance={onRemoveSavedInstance}
        />
      )}
    </RootStack.Screen>
  );

  const renderLoginScreen = () => (
    <RootStack.Screen name="Login">
      {() => (
        <LoginScreen
          appDisplayName={appDisplayName}
          brandColor={brandColor}
          logoUrl={logoUrl}
          configuredBaseUrl={configuredBaseUrl ?? ""}
          inviteToken={inviteToken}
          email={email}
          password={password}
          submitting={submitting}
          error={error}
          statusMessage={statusMessage}
          onChangeEmail={setEmail}
          onChangePassword={setPassword}
          onLogin={() => {
            onLogin();
          }}
          onChangeInstance={onChangeInstance}
        />
      )}
    </RootStack.Screen>
  );

  const renderRootScreen = () => {
    if (configuredBaseUrl && user) {
      return <RootStack.Screen name="AppNavigator">{renderAppNavigator}</RootStack.Screen>;
    }
    if (configuredBaseUrl) {
      return renderLoginScreen();
    }
    return renderOnboardingScreen();
  };

  return (
    <SafeAreaProvider>
      <NavigationContainer ref={navigationRef}>
        <RootStack.Navigator screenOptions={{ headerShown: false }}>
          {renderRootScreen()}
        </RootStack.Navigator>
      </NavigationContainer>
    </SafeAreaProvider>
  );
}

// Racine : garde-fou de rendu autour de toute l'app. Une erreur levee pendant le
// rendu affiche un ecran de repli (cf. ErrorBoundary) au lieu d'un ecran blanc.
export default function App() {
  return (
    <ErrorBoundary>
      <UpdateGate>
        <AppInner />
      </UpdateGate>
    </ErrorBoundary>
  );
}

const styles = StyleSheet.create({
  centered: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    gap: 10,
    backgroundColor: "#edf2f8",
  },
  muted: {
    color: "#6a7a96",
  },
  subScreen: {
    flex: 1,
    backgroundColor: "#edf2f8",
    padding: 16,
    gap: 10,
  },
  status: {
    color: "#1c7f45",
    marginTop: 4,
  },
});
