import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import {
  ActivityIndicator,
  Alert,
  AppState,
  Image,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  View,
} from "react-native";
import { useNavigation, useRoute, type RouteProp } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
  completeStepLocally,
  loadStepsLocally,
  validateStepValue,
} from "../../core/stepRepository";
import {
  clearWorkflowDraft,
  enqueueSyncAction,
  loadWorkflowDraft,
  saveWorkflowDraft,
  type WorkflowDraft,
} from "../../core/localDatabase";
import { isNetworkOnline } from "../../services/syncService";
import { resolveMediaFileUri } from "../../core/fileStorage";
import {
  completeMobileWorkflow,
  fetchAndStoreInterventionSteps,
  getInterventionById,
  saveStepToServer,
  uploadMachinePhoto,
} from "../../services/interventionsApi";
import {
  getDevisForMachine,
  openDevisPdf,
  type DevisItem,
} from "../../services/devisApi";
import SignatureFinishCard from "./SignatureFinishCard";
import type {
  MobileStep,
  StepPhotoValue,
  StepSignatureValue,
  StepValue,
} from "../../types/intervention";
import type { AppStackParamList } from "../../navigation/types";
import { CameraView, useCameraPermissions } from "expo-camera";
import CameraCapture from "../../shared/CameraCapture";
import SignatureCapture from "../../shared/SignatureCapture";
import {
  buildPhases,
  computeVisibleStepIds,
  getBlockingStepIds,
  getCommentRequirement,
  getStepMetadata,
  isAutoCalc,
  isCerfaLike,
  isDevisListStep,
  isDisplayOnly,
  isEmptyStepValue,
  isGateStep,
  resolveDevisStepText,
  selectDevisForStep,
  uuidv4,
  type WorkflowPhase,
} from "../../shared/workflowUtils";

type WorkflowRoute = RouteProp<AppStackParamList, "WorkflowIntervention">;
type WorkflowNav = NativeStackNavigationProp<AppStackParamList, "WorkflowIntervention">;

// Devis de la machine, charges une fois au niveau ecran et exposes aux etapes
// d'affichage "LISTE DEVIS *" (DisplayStepInput) sans threader la liste a
// travers StepCard/StepInput. Defaut = liste vide (silent-fail / pas de devis).
const DevisListContext = createContext<DevisItem[]>([]);

function getInitialValueForStep(step: MobileStep): StepValue {
  if (step.value !== null && step.value !== undefined) {
    return step.value;
  }

  if (step.type === "photo") {
    return [];
  }

  // Pour un boolean ou un choice on commence par "" (rien selectionne)
  // pour matcher le PWA qui propose une option vide "Selectionner..." par
  // defaut. Inutile de poser false/true qui seraient interpretes comme
  // une selection deja faite.
  return "";
}

function isPhotoArrayValue(value: StepValue): value is StepPhotoValue[] {
  return Array.isArray(value);
}

function isSignatureValue(value: StepValue): value is StepSignatureValue {
  return Boolean(value && typeof value === "object" && !Array.isArray(value) && "uri" in value);
}

function getStepInputKind(step: MobileStep): string {
  const m = (step.metadata || {}) as { input_kind?: string };
  return String(m.input_kind || "");
}

function shouldSkipStepForSave(step: MobileStep): boolean {
  const kind = getStepInputKind(step);
  const isDisplay = kind === "display" || kind === "section" || kind === "info";
  return isDisplay || isAutoCalc(step) || kind === "photo";
}

function collectBlockingRequiredErrors(
  phase: WorkflowPhase,
  inputValues: Record<string, StepValue>,
  visibleStepIds: Set<string>
): Record<string, string> {
  const blocking = getBlockingStepIds(phase, inputValues, visibleStepIds);
  const errors: Record<string, string> = {};
  for (const id of blocking) {
    errors[id] = "Champ requis";
  }
  return errors;
}

function validateStepForPhase(
  step: MobileStep,
  inputValues: Record<string, StepValue>,
  commentValues: Record<string, string>
): string | null {
  if (isAutoCalc(step) || isDisplayOnly(step)) {
    return null;
  }
  const raw = inputValues[step.id] ?? step.value;
  const value: StepValue =
    step.type === "measurement" && raw !== null && raw !== undefined
      ? Number(raw)
      : raw;
  const err = validateStepValue(step, value);
  if (err) {
    return err;
  }
  const commentReq = getCommentRequirement(step, raw);
  if (commentReq.required && (commentValues[step.id] ?? "").trim() === "") {
    return "Commentaire obligatoire pour decrire le probleme/panne";
  }
  return null;
}

function collectConstraintErrors(
  phase: WorkflowPhase,
  inputValues: Record<string, StepValue>,
  commentValues: Record<string, string>,
  visibleStepIds: Set<string>
): Record<string, string> {
  const errors: Record<string, string> = {};
  for (const step of phase.steps) {
    if (!visibleStepIds.has(step.id)) {
      continue;
    }
    const err = validateStepForPhase(step, inputValues, commentValues);
    if (err) {
      errors[step.id] = err;
    }
  }
  return errors;
}

function buildSaveStepOfflinePayload(
  step: MobileStep,
  interventionId: number,
  raw: StepValue,
  cmt: string,
  version: number,
  idempotencyKey: string
) {
  return {
    endpoint: "/mobile/intervention_workflow.php",
    method: "POST" as const,
    payload: {
      action: "save_step",
      intervention_id: interventionId,
      step_key: step.id,
      value: raw ?? null,
      comment: cmt || null,
      version,
      idempotency_key: idempotencyKey,
    },
    entityType: "step",
    entityId: `${interventionId}:${step.id}`,
  };
}

function CommentInput({
  stepId,
  commentReq,
  value,
  onChange,
}: Readonly<{
  stepId: string;
  commentReq: ReturnType<typeof getCommentRequirement>;
  value: string;
  onChange: (stepId: string, text: string) => void;
}>) {
  const label = commentReq.required ? (
    <>
      Description du probleme/panne <Text style={styles.requiredMark}>(obligatoire)</Text>
    </>
  ) : (
    "Commentaire (si Probleme/Panne)"
  );
  return (
    <View style={styles.commentWrap}>
      <Text style={styles.commentLabel}>{label}</Text>
      <TextInput
        style={[styles.textInput, styles.textArea]}
        value={value}
        onChangeText={(text) => onChange(stepId, text)}
        placeholder={commentReq.placeholder}
        placeholderTextColor="#94a3b8"
        multiline
        numberOfLines={3}
      />
    </View>
  );
}

function StepCard({
  step,
  index,
  interventionId,
  brandColor,
  currentValue,
  commentValue,
  validationError,
  onChangeValue,
  onChangeComment,
}: Readonly<{
  step: MobileStep;
  index: number;
  interventionId: number;
  brandColor: string;
  currentValue: StepValue;
  commentValue: string;
  validationError: string | undefined;
  onChangeValue: (stepId: string, v: StepValue) => void;
  onChangeComment: (stepId: string, text: string) => void;
}>) {
  const meta = getStepMetadata(step);
  const isExt = Boolean(meta.is_extension);
  const commentReq = getCommentRequirement(step, currentValue);

  return (
    <View style={[styles.stepCard, isExt && styles.stepCardExtension]}>
      {isExt ? (
        <View style={[styles.extensionBadge, { backgroundColor: brandColor }]}>
          <Text style={styles.extensionBadgeText}>↳ Sous-etape</Text>
        </View>
      ) : null}
      <View style={styles.stepHeader}>
        <View style={styles.stepMeta}>
          <View
            style={[styles.stepIndex, { backgroundColor: isExt ? "#94a3b8" : brandColor }]}
          >
            <Text style={styles.stepIndexText}>{index + 1}</Text>
          </View>
          <View style={styles.stepInfo}>
            <Text style={styles.stepLabel}>
              {step.label}
              {step.required ? <Text style={styles.requiredMark}> *</Text> : null}
            </Text>
          </View>
        </View>
      </View>
      <View style={styles.stepBody}>
        <StepInput
          interventionId={interventionId}
          step={step}
          value={currentValue}
          onChange={(v) => onChangeValue(step.id, v)}
          brandColor={brandColor}
        />
        {commentReq.visible ? (
          <CommentInput
            stepId={step.id}
            commentReq={commentReq}
            value={commentValue}
            onChange={onChangeComment}
          />
        ) : null}
        {validationError ? (
          <Text style={styles.validationError}>{validationError}</Text>
        ) : null}
      </View>
    </View>
  );
}

function hydrateInitialValues(
  prev: Record<string, StepValue>,
  local: MobileStep[]
): Record<string, StepValue> {
  const next = { ...prev };
  for (const step of local) {
    if (next[step.id] === undefined) {
      next[step.id] = getInitialValueForStep(step);
    }
  }
  return next;
}

function hydrateInitialComments(
  prev: Record<string, string>,
  local: MobileStep[]
): Record<string, string> {
  const next = { ...prev };
  for (const step of local) {
    if (next[step.id] === undefined) {
      const meta = (step.metadata || {}) as { comment_text?: string | null };
      next[step.id] = String(meta.comment_text ?? "");
    }
  }
  return next;
}

function useWorkflowDraftPersistence(
  interventionId: number,
  hydratedRef: React.RefObject<boolean>,
  draft: {
    inputValues: Record<string, StepValue>;
    commentValues: Record<string, string>;
    currentPhaseIdx: number;
    sigTechData: string;
    sigClientData: string;
    sigClientPresent: boolean;
    showSignatureCard: boolean;
  }
): void {
  // Ref vers le dernier draft : permet au flush "arriere-plan" d'ecrire la
  // valeur courante sans dependre de la closure (qui serait perimee).
  const latestDraftRef = useRef(draft);
  latestDraftRef.current = draft;

  const persistNow = useCallback(() => {
    if (!hydratedRef.current) return;
    const d = latestDraftRef.current;
    saveWorkflowDraft(interventionId, {
      inputValues: d.inputValues as Record<string, unknown>,
      commentValues: d.commentValues,
      currentPhaseIdx: d.currentPhaseIdx,
      sigTechData: d.sigTechData,
      sigClientData: d.sigClientData,
      sigClientPresent: d.sigClientPresent,
      showSignatureCard: d.showSignatureCard,
      updatedAt: new Date().toISOString(),
    });
  }, [interventionId, hydratedRef]);

  useEffect(() => {
    if (!hydratedRef.current) return;
    const timer = setTimeout(persistNow, 400);
    return () => clearTimeout(timer);
  }, [
    persistNow,
    hydratedRef,
    draft.inputValues,
    draft.commentValues,
    draft.currentPhaseIdx,
    draft.sigTechData,
    draft.sigClientData,
    draft.sigClientPresent,
    draft.showSignatureCard,
  ]);

  // M5 : flush IMMEDIAT au passage en arriere-plan / inactif. Le debounce 400ms
  // perdait la derniere frappe si l'app etait tuee juste apres (kill < 400ms).
  // On ecrit donc le draft sans attendre des que l'app quitte le premier plan.
  useEffect(() => {
    const sub = AppState.addEventListener("change", (state) => {
      if (state === "background" || state === "inactive") {
        persistNow();
      }
    });
    return () => sub.remove();
  }, [persistNow]);

  // #6 : flush a la sortie d'ecran (unmount / navigation interne). Sans ca, la
  // derniere frappe restee dans la fenetre de debounce de 400ms etait perdue en
  // quittant l'ecran (le passage en arriere-plan ne couvre pas la nav interne).
  useEffect(() => {
    return () => {
      persistNow();
    };
  }, [persistNow]);
}

function MachinePhotoOverlay({
  saving,
  cameraOpen,
  brandColor,
  cameraRef,
  onTake,
  onOpen,
  onSkip,
}: Readonly<{
  saving: boolean;
  cameraOpen: boolean;
  brandColor: string;
  cameraRef: React.RefObject<CameraView | null>;
  onTake: () => void;
  onOpen: () => void;
  onSkip: () => void;
}>) {
  const renderBody = () => {
    // La CameraView ne doit JAMAIS etre demontee pendant une capture : sinon la
    // session camera native est detruite en plein takePictureAsync -> la
    // promesse ne resout pas (loader fige) ou la preview revient noire au
    // remontage. On garde donc la camera montee tant que cameraOpen et on
    // superpose le loader quand saving — miroir de CameraCapture, dont la
    // CameraView est gouvernee par le Modal visible et non par le flag capturing.
    if (cameraOpen) {
      return (
        <View style={{ flex: 1 }}>
          <CameraView
            ref={cameraRef}
            testID="machine-camera"
            style={{ flex: 1, borderRadius: 12 }}
            facing="back"
          />
          {saving ? (
            <View style={styles.machinePhotoLoadingOverlay}>
              <ActivityIndicator size="large" color="#ffffff" />
              <Text style={{ marginTop: 12, color: "#ffffff" }}>Enregistrement...</Text>
            </View>
          ) : (
            <Pressable
              style={[styles.machinePhotoSkip, { backgroundColor: brandColor, marginTop: 12 }]}
              onPress={onTake}
            >
              <Text style={[styles.machinePhotoSkipText, { color: "#fff" }]}>📷 Capturer</Text>
            </Pressable>
          )}
        </View>
      );
    }
    // Camera fermee + saving = upload en cours apres capture : loader plein cadre.
    if (saving) {
      return (
        <View style={styles.machinePhotoLoading}>
          <ActivityIndicator size="large" color={brandColor} />
          <Text style={{ marginTop: 12, color: "#6b7280" }}>Enregistrement...</Text>
        </View>
      );
    }
    return (
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Pressable
          style={[
            styles.machinePhotoSkip,
            { backgroundColor: brandColor, paddingVertical: 16, paddingHorizontal: 32 },
          ]}
          onPress={onOpen}
        >
          <Text style={[styles.machinePhotoSkipText, { color: "#fff", fontSize: 16 }]}>
            📷 Ouvrir la camera
          </Text>
        </Pressable>
      </View>
    );
  };

  return (
    <View style={styles.machinePhotoOverlay}>
      <View style={styles.machinePhotoHeader}>
        <Text style={styles.machinePhotoTitle}>📷 Photo de la machine</Text>
        <Text style={styles.machinePhotoSubtitle}>
          Aucune photo n'est enregistrée pour cette machine.{"\n"}
          Prenez une photo pour qu'elle apparaisse sur les futurs rapports.
        </Text>
      </View>
      {renderBody()}
      <Pressable style={styles.machinePhotoSkip} onPress={onSkip}>
        <Text style={styles.machinePhotoSkipText}>Passer →</Text>
      </Pressable>
    </View>
  );
}

// Resout le machine_id pour le chargement des devis. machineId vient des params
// de navigation (Number(item.machine_id)) ; s'il est absent/0 (ecran ouvert sans
// detail charge), on retombe sur le detail intervention (cache offline-first).
// Silent-fail -> 0 (liste vide).
async function resolveDevisMachineId(
  machineId: number | undefined,
  interventionId: number
): Promise<number> {
  const fromParams = Number(machineId) || 0;
  if (fromParams) {
    return fromParams;
  }
  try {
    const detail = await getInterventionById(interventionId);
    return Number((detail as { machine_id?: unknown })?.machine_id) || 0;
  } catch {
    // detail indispo -> on reste a 0, liste vide (silent-fail).
    return 0;
  }
}

export default function WorkflowInterventionScreen() {
  const route = useRoute<WorkflowRoute>();
  const navigation = useNavigation<WorkflowNav>();
  const insets = useSafeAreaInsets();
  const { interventionId, brandColor, machineId, machineHasPhoto: initialMachineHasPhoto } = route.params;

  const [steps, setSteps] = useState<MobileStep[]>([]);
  const [loading, setLoading] = useState(true);
  const [inputValues, setInputValues] = useState<Record<string, StepValue>>({});
  const [commentValues, setCommentValues] = useState<Record<string, string>>({});
  const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
  const [currentPhaseIdx, setCurrentPhaseIdx] = useState(0);
  const [finishing, setFinishing] = useState(false);
  const [finishError, setFinishError] = useState<string | null>(null);
  const [phaseAlert, setPhaseAlert] = useState<string | null>(null);
  // Carte signature (overlay) : visible quand l'utilisateur a passe avec
  // succes la validation de la derniere phase et doit signer pour finaliser.
  const [showSignatureCard, setShowSignatureCard] = useState(false);
  const [workflowVersion, setWorkflowVersion] = useState(0);
  // State signatures remonte ici (et pas dans SignatureFinishCard) pour que
  // les data URL survivent a un "Revenir" qui demonte le composant. Si le
  // tech navigue vers la phase precedente puis revient a la finalisation,
  // il retrouve ses signatures au lieu de devoir tout re-signer.
  const [sigTechData, setSigTechData] = useState<string>("");
  const [sigClientData, setSigClientData] = useState<string>("");
  const [sigClientPresent, setSigClientPresent] = useState<boolean>(true);

  // Photo machine : intercalee entre derniere phase et signatures si la machine
  // n'a pas de photo. Le technicien peut prendre une photo ou passer.
  const [showMachinePhotoCard, setShowMachinePhotoCard] = useState(false);
  const [machineHasPhoto, setMachineHasPhoto] = useState(initialMachineHasPhoto ?? false);
  const [machinePhotoSaving, setMachinePhotoSaving] = useState(false);
  const [machinePhotoCameraOpen, setMachinePhotoCameraOpen] = useState(false);
  const [cameraPermission, requestCameraPermission] = useCameraPermissions();
  const machineCameraRef = useRef<CameraView | null>(null);

  // Devis de la machine : charges uniquement si le workflow contient au moins
  // une etape d'affichage "LISTE DEVIS *", pour ne pas solliciter /devis.php
  // sur les interventions qui n'en ont pas. Silent-fail -> liste vide.
  const [devisList, setDevisList] = useState<DevisItem[]>([]);

  // Groupement par phases, miroir de la PWA (buildPhases dans workflow-utils.js).
  // Les etapes gate ("Commencer ?") sont filtrees, les extensions conservees
  // (leur visibilite est calculee a chaque frame).
  const phases = useMemo(() => buildPhases(steps), [steps]);
  const currentPhase = phases[currentPhaseIdx] ?? null;

  // Extensions : visibilite recalculee a chaque changement de valeur.
  // Exemple : etape "Etat compresseur" = "Panne" -> ses sous-etapes
  // s'affichent instantanement.
  const visibleStepIds = useMemo(() => {
    if (!currentPhase) {
      return new Set<string>();
    }
    return computeVisibleStepIds(currentPhase, inputValues);
  }, [currentPhase, inputValues]);

  const isLastPhase = phases.length > 0 && currentPhaseIdx >= phases.length - 1;

  // Charge les devis de la machine une fois que les steps sont connus et qu'au
  // moins une etape "LISTE DEVIS *" est presente. Silent-fail (mirror PWA).
  //
  // NOTE: fallback client-side. Le rendu privilegie metadata.display_content
  // (texte resolu cote serveur, cf. handoff #155) quand le backend l'expose ;
  // ce fetch ne sert que si le backend laisse la resolution au client. Il est
  // actuellement bloque par un 401 sur /api/devis.php (endpoint partage PWA :
  // le Bearer mobile est rejete car le TenantContext n'est pas resolu avant la
  // validation du JWT sur ce chemin). Silent-fail -> on retombe sur le texte
  // "Aucun devis ..." tant que le backend n'a pas tranche (fix 401 ou
  // display_content). Cf. thread handoff a `app`.
  const hasDevisSteps = useMemo(() => steps.some(isDevisListStep), [steps]);
  useEffect(() => {
    if (!hasDevisSteps) {
      return;
    }
    let active = true;
    (async () => {
      const mid = await resolveDevisMachineId(machineId, interventionId);
      if (!mid) {
        return;
      }
      const list = await getDevisForMachine(mid);
      if (active) {
        setDevisList(list);
      }
    })();
    return () => {
      active = false;
    };
  }, [hasDevisSteps, machineId, interventionId]);

  const loadSteps = useCallback(async () => {
    setLoading(true);
    // Refresh depuis le serveur d'abord pour avoir la version a jour du
    // workflow (necessaire pour complete_workflow). Best-effort : si offline
    // on retombe sur le local.
    try {
      const remote = await fetchAndStoreInterventionSteps(interventionId);
      setWorkflowVersion(remote.workflowVersion);
    } catch {
      // ignore : on continue avec le local
    }
    const local = await loadStepsLocally(interventionId);
    setSteps(local);
    setInputValues((prev) => hydrateInitialValues(prev, local));
    setCommentValues((prev) => hydrateInitialComments(prev, local));
    setLoading(false);
  }, [interventionId]);

  useEffect(() => {
    loadSteps();
  }, [loadSteps]);

  // Hydrate le draft persistant (SQLite) au mount : valeurs, commentaires,
  // signatures, phase courante, presence client. Permet de reprendre
  // l'intervention la ou le tech s'etait arrete si l'app a ete tuee
  // brusquement. Declenche UNE fois par intervention — les set* suivants
  // ne re-hydratent pas pour ne pas ecraser les modifs en cours.
  const hydratedOnceRef = useRef(false);

  const applyDraftHydration = useCallback((draft: WorkflowDraft) => {
    if (draft.inputValues) {
      // Cast draft.inputValues (unknown) vers StepValue - on fait confiance
      // a la serialisation precedente qui ne stocke que des StepValue.
      setInputValues((prev) => ({ ...(draft.inputValues as Record<string, StepValue>), ...prev }));
    }
    if (draft.commentValues) {
      setCommentValues((prev) => ({ ...draft.commentValues, ...prev }));
    }
    if (typeof draft.currentPhaseIdx === "number") {
      setCurrentPhaseIdx(draft.currentPhaseIdx);
    }
    if (draft.sigTechData) setSigTechData(draft.sigTechData);
    if (draft.sigClientData) setSigClientData(draft.sigClientData);
    if (typeof draft.sigClientPresent === "boolean") setSigClientPresent(draft.sigClientPresent);
    if (draft.showSignatureCard) setShowSignatureCard(true);
  }, []);

  useEffect(() => {
    if (hydratedOnceRef.current) {
      return;
    }
    (async () => {
      const draft = await loadWorkflowDraft(interventionId);
      if (draft) {
        applyDraftHydration(draft);
      }
      hydratedOnceRef.current = true;
    })();
  }, [interventionId, applyDraftHydration]);

  // Persiste le draft en SQLite apres chaque modification (debounce 400ms
  // pour eviter les ecritures trop frequentes pendant la frappe). Si l'app
  // crash, le prochain mount rehydrate exactement ou on etait.
  useWorkflowDraftPersistence(interventionId, hydratedOnceRef, {
    inputValues,
    commentValues,
    currentPhaseIdx,
    sigTechData,
    sigClientData,
    sigClientPresent,
    showSignatureCard,
  });

  /**
   * Sauvegarde toutes les etapes visibles et validees de la phase courante,
   * puis fait avancer vers la phase suivante. Miroir du bouton "Suivant"
   * de intervention_etapes.php — batch de saves + nextStep.
   */
  const persistSingleStep = useCallback(
    async (
      step: MobileStep,
      raw: StepValue,
      cmt: string,
      version: number,
      online: boolean
    ): Promise<number> => {
      const idempotencyKey = uuidv4();
      if (online) {
        const result = await saveStepToServer({
          interventionId,
          stepKey: step.id,
          value: raw ?? null,
          comment: cmt || null,
          version,
          idempotencyKey,
        });
        return result.version;
      }
      await enqueueSyncAction(
        buildSaveStepOfflinePayload(step, interventionId, raw, cmt, version, idempotencyKey)
      );
      return version;
    },
    [interventionId]
  );

  const applyLocalStepUpdate = useCallback(
    async (step: MobileStep, raw: StepValue, cmt: string): Promise<void> => {
      const updated = await completeStepLocally(step, raw ?? null);
      setSteps((prev) =>
        prev.map((s) => {
          if (s.id !== step.id) return s;
          const mergedMetadata: Record<string, unknown> = { ...s.metadata, comment_text: cmt };
          return { ...updated, metadata: mergedMetadata };
        })
      );
    },
    []
  );

  const savePhaseSteps = useCallback(
    async (phase: WorkflowPhase, online: boolean, startVersion: number): Promise<
      { ok: true; version: number } | { ok: false; stepId: string; message: string }
    > => {
      let currentVersion = startVersion;
      for (const step of phase.steps) {
        if (!visibleStepIds.has(step.id) || shouldSkipStepForSave(step)) {
          continue;
        }
        const raw = inputValues[step.id];
        const cmt = (commentValues[step.id] ?? "").trim();
        const hasValue = raw !== undefined && !isEmptyStepValue(raw);
        if (!hasValue && cmt === "") {
          continue;
        }
        try {
          currentVersion = await persistSingleStep(step, raw, cmt, currentVersion, online);
          await applyLocalStepUpdate(step, raw, cmt);
        } catch (err) {
          const message = err instanceof Error ? err.message : "Echec sauvegarde serveur";
          return { ok: false, stepId: step.id, message };
        }
      }
      return { ok: true, version: currentVersion };
    },
    [visibleStepIds, inputValues, commentValues, persistSingleStep, applyLocalStepUpdate]
  );

  const autoSaveGateSteps = useCallback(
    async (allSteps: MobileStep[], startVersion: number, online: boolean): Promise<number> => {
      let currentVersion = startVersion;
      for (const step of allSteps) {
        if (!isGateStep(step) || !isEmptyStepValue(step.value)) {
          continue;
        }
        try {
          if (online) {
            const result = await saveStepToServer({
              interventionId,
              stepKey: step.id,
              value: "1",
              comment: null,
              version: currentVersion,
              idempotencyKey: uuidv4(),
            });
            currentVersion = result.version;
          } else {
            // #2 : HORS-LIGNE, on ENFILE le gate (valeur "1") au lieu de le
            // sauter. Sinon un gate requis franchi hors-ligne n'etait jamais
            // sauvegarde -> complete_workflow le rejetait en 422 a la
            // finalisation. La version sera rebasee au replay (cf. syncService).
            await enqueueSyncAction(
              buildSaveStepOfflinePayload(step, interventionId, "1", "", currentVersion, uuidv4())
            );
          }
        } catch {
          // best-effort : server renverra l'erreur precise a complete_workflow.
        }
      }
      return currentVersion;
    },
    [interventionId]
  );

  const refreshStepsFromServer = useCallback(async (): Promise<void> => {
    try {
      const remote = await fetchAndStoreInterventionSteps(interventionId);
      setWorkflowVersion(remote.workflowVersion);
      const fresh = await loadStepsLocally(interventionId);
      setSteps(fresh);
    } catch {
      // best-effort
    }
  }, [interventionId]);

  const savePhaseAndAdvance = useCallback(async (): Promise<boolean> => {
    if (!currentPhase) {
      return false;
    }

    const blockingErrors = collectBlockingRequiredErrors(currentPhase, inputValues, visibleStepIds);
    if (Object.keys(blockingErrors).length > 0) {
      setValidationErrors((prev) => ({ ...prev, ...blockingErrors }));
      setPhaseAlert("Merci de renseigner toutes les etapes requises avant de passer a la suivante.");
      return false;
    }

    const constraintErrors = collectConstraintErrors(
      currentPhase,
      inputValues,
      commentValues,
      visibleStepIds
    );
    if (Object.keys(constraintErrors).length > 0) {
      setValidationErrors((prev) => ({ ...prev, ...constraintErrors }));
      setPhaseAlert("Certaines valeurs sont invalides.");
      return false;
    }

    const online = await isNetworkOnline();
    const saveResult = await savePhaseSteps(currentPhase, online, workflowVersion);
    if (!saveResult.ok) {
      setValidationErrors((prev) => ({ ...prev, [saveResult.stepId]: saveResult.message }));
      setPhaseAlert("Erreur lors de la sauvegarde de la phase. Verifiez la connexion et reessayez.");
      return false;
    }

    // #2 : on auto-sauve les gates en ligne ET hors-ligne (enqueue offline) pour
    // qu'un gate franchi sans reseau ne soit pas perdu (sinon 422 a la finalisation).
    let currentVersion = saveResult.version;
    currentVersion = await autoSaveGateSteps(steps, currentVersion, online);
    setWorkflowVersion(currentVersion);

    if (online) {
      await refreshStepsFromServer();
    }

    setPhaseAlert(null);
    return true;
  }, [
    currentPhase,
    inputValues,
    commentValues,
    visibleStepIds,
    workflowVersion,
    steps,
    savePhaseSteps,
    autoSaveGateSteps,
    refreshStepsFromServer,
  ]);

  const handleNextPhase = useCallback(async () => {
    const ok = await savePhaseAndAdvance();
    if (!ok) {
      return;
    }
    if (isLastPhase) {
      // Si la machine n'a pas de photo, on intercale l'ecran photo
      // avant les signatures (meme logique que la PWA).
      if (!machineHasPhoto) {
        setShowMachinePhotoCard(true);
        setFinishError(null);
        return;
      }
      setShowSignatureCard(true);
      setFinishError(null);
      return;
    }
    setCurrentPhaseIdx((idx) => Math.min(idx + 1, phases.length - 1));
  }, [savePhaseAndAdvance, isLastPhase, phases.length]);

  const handlePrevPhase = useCallback(() => {
    setPhaseAlert(null);
    if (currentPhaseIdx === 0) {
      navigation.goBack();
      return;
    }
    setCurrentPhaseIdx((idx) => Math.max(0, idx - 1));
  }, [currentPhaseIdx, navigation]);

  // ── Handlers photo machine ────────────────────────────────────
  const openMachineCamera = useCallback(async () => {
    if (!cameraPermission?.granted) {
      const result = await requestCameraPermission();
      if (!result.granted) {
        setPhaseAlert("Permission camera refusee. Vous pouvez passer cette etape.");
        return;
      }
    }
    setMachinePhotoCameraOpen(true);
  }, [cameraPermission, requestCameraPermission]);

  const takeMachinePhoto = useCallback(async () => {
    if (!machineCameraRef.current || machinePhotoSaving) return;
    setMachinePhotoSaving(true);
    try {
      // base64:true -> la camera renvoie directement le base64 de la photo. On
      // EVITE volontairement le chemin fetch(file://) + Blob + FileReader, qui
      // hang ou throw sur certains Android et laissait le tech bloque (ecran
      // noir / capture figee). quality 0.5 pour limiter la taille du payload.
      const photo = await machineCameraRef.current.takePictureAsync({
        quality: 0.5,
        base64: true,
      });
      if (!photo?.base64) {
        setPhaseAlert("Capture invalide, reprenez la photo.");
        setMachinePhotoSaving(false);
        return;
      }

      setMachinePhotoCameraOpen(false);

      // Le backend exige un data-URI prefixe (data:image/...). Cf
      // InterventionWorkflowApiController::handleUploadMachinePhoto.
      const dataUri = `data:image/jpeg;base64,${photo.base64}`;

      const online = await isNetworkOnline();
      if (online) {
        await uploadMachinePhoto(interventionId, dataUri);
      } else {
        await enqueueSyncAction({
          endpoint: "/mobile/intervention_workflow.php",
          method: "POST",
          payload: {
            action: "upload_machine_photo",
            intervention_id: interventionId,
            photo: dataUri,
          },
          entityType: "machine_photo",
          entityId: String(machineId || interventionId),
        });
      }
      setMachineHasPhoto(true);
      setShowMachinePhotoCard(false);
      setShowSignatureCard(true);
    } catch (err) {
      setPhaseAlert(
        "Erreur photo : " + (err instanceof Error ? err.message : String(err))
      );
    } finally {
      setMachinePhotoSaving(false);
    }
  }, [interventionId, machineId, machinePhotoSaving]);

  const handleSkipMachinePhoto = useCallback(() => {
    setShowMachinePhotoCard(false);
    setShowSignatureCard(true);
    setFinishError(null);
  }, []);

  /**
   * Appele depuis SignatureFinishCard quand l'utilisateur a signe et
   * confirme. Mirror PWA confirmSignature -> callCompleteWorkflow :
   *   - online : POST direct /mobile/intervention_workflow.php
   *     (server completeWorkflow s'occupe du PDF + demande devis)
   *   - offline : enqueue dans sync_queue, sera rejoue a la reconnexion
   */
  const handleFinishWithSignatures = useCallback(
    async (signatures: {
      signature_technicien: string;
      signature_client: string;
      client_present: boolean;
    }) => {
      setFinishing(true);
      setFinishError(null);
      try {
        const online = await isNetworkOnline();
        if (online) {
          await completeMobileWorkflow(interventionId, workflowVersion, signatures);
          // Finalisation serveur reussie : on peut supprimer le draft local,
          // l'intervention est terminee, plus besoin de le restaurer.
          await clearWorkflowDraft(interventionId);
        } else {
          // Offline : on enqueue le complete_workflow avec les signatures.
          // Le sync service rejouera ca a la reconnexion.
          await enqueueSyncAction({
            endpoint: "/mobile/intervention_workflow.php",
            method: "POST",
            payload: {
              action: "complete_workflow",
              intervention_id: interventionId,
              version: workflowVersion,
              signature_technicien: signatures.signature_technicien,
              signature_client: signatures.signature_client,
              client_present: signatures.client_present,
            },
            entityType: "intervention",
            entityId: String(interventionId),
          });
        }
        navigation.goBack();
      } catch (err) {
        setFinishError(
          err instanceof Error ? err.message : "Erreur lors de la finalisation"
        );
      } finally {
        setFinishing(false);
      }
    },
    [interventionId, workflowVersion, navigation]
  );

  const visibleSteps = currentPhase
    ? currentPhase.steps.filter((s) => visibleStepIds.has(s.id))
    : [];
  const nextActionLabel = isLastPhase ? "✅ Terminer la mission" : "Suivant →";
  const backLabel = currentPhaseIdx === 0 ? "Retour" : "← Revenir";

  const handleSignatureDraftChange = (d: {
    techData?: string;
    clientData?: string;
    clientPresent?: boolean;
  }) => {
    if (d.techData !== undefined) setSigTechData(d.techData);
    if (d.clientData !== undefined) setSigClientData(d.clientData);
    if (d.clientPresent !== undefined) setSigClientPresent(d.clientPresent);
  };
  const handleSignatureCardCancel = () => {
    setShowSignatureCard(false);
    setFinishError(null);
  };

  return (
    <View style={styles.root}>
      {showMachinePhotoCard ? (
        <MachinePhotoOverlay
          saving={machinePhotoSaving}
          cameraOpen={machinePhotoCameraOpen}
          brandColor={brandColor}
          cameraRef={machineCameraRef}
          onTake={takeMachinePhoto}
          onOpen={openMachineCamera}
          onSkip={handleSkipMachinePhoto}
        />
      ) : null}

      {showSignatureCard ? (
        <SignatureFinishCard
          brandColor={brandColor}
          saving={finishing}
          errorMessage={finishError}
          initialTechData={sigTechData}
          initialClientData={sigClientData}
          initialClientPresent={sigClientPresent}
          onDraftChange={handleSignatureDraftChange}
          onCancel={handleSignatureCardCancel}
          onConfirm={handleFinishWithSignatures}
        />
      ) : null}

      <ScrollView
        style={styles.scroll}
        contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 24 }]}
        showsVerticalScrollIndicator={false}
      >
        <Text style={styles.pageTitle}>Intervention #{interventionId}</Text>

        {loading ? (
          <View style={styles.loaderRow}>
            <ActivityIndicator size="small" />
            <Text style={styles.mutedText}>Chargement des etapes...</Text>
          </View>
        ) : null}

        {!loading && phases.length === 0 ? (
          <Text style={styles.mutedText}>Aucune etape disponible pour cette intervention.</Text>
        ) : null}

        {!loading && currentPhase ? (
          <>
            {/* Fil d'ariane de phase, miroir du phaseStrip PWA */}
            <View style={[styles.phaseStrip, { borderColor: brandColor }]}>
              <Text style={[styles.phaseStripCounter, { color: brandColor }]}>
                Phase {currentPhaseIdx + 1} / {phases.length}
              </Text>
              <Text style={styles.phaseStripName}>{currentPhase.name}</Text>
            </View>

            {phaseAlert ? <Text style={styles.phaseAlert}>{phaseAlert}</Text> : null}

            <DevisListContext.Provider value={devisList}>
              {visibleSteps.map((step, index) => (
                <StepCard
                  key={step.id}
                  step={step}
                  index={index}
                  interventionId={interventionId}
                  brandColor={brandColor}
                  currentValue={inputValues[step.id] ?? getInitialValueForStep(step)}
                  commentValue={commentValues[step.id] ?? ""}
                  validationError={validationErrors[step.id]}
                  onChangeValue={(stepId, v) =>
                    setInputValues((prev) => ({ ...prev, [stepId]: v }))
                  }
                  onChangeComment={(stepId, text) =>
                    setCommentValues((prev) => ({ ...prev, [stepId]: text }))
                  }
                />
              ))}
            </DevisListContext.Provider>
          </>
        ) : null}

        {finishError ? <Text style={styles.finishError}>{finishError}</Text> : null}
      </ScrollView>

      <View style={[styles.actionsBar, { paddingBottom: insets.bottom + 16 }]}>
        <Pressable style={styles.backButton} onPress={handlePrevPhase}>
          <Text style={styles.backButtonText}>{backLabel}</Text>
        </Pressable>
        <Pressable
          style={[
            styles.finishButton,
            { backgroundColor: isLastPhase && !finishing ? "#16a34a" : brandColor },
            (phases.length === 0 || finishing) && { backgroundColor: "#cbd5e1" },
          ]}
          disabled={phases.length === 0 || finishing}
          onPress={() => {
            handleNextPhase();
          }}
        >
          {finishing ? (
            <ActivityIndicator size="small" color="#ffffff" />
          ) : (
            <Text style={styles.finishButtonText}>{nextActionLabel}</Text>
          )}
        </Pressable>
      </View>
    </View>
  );
}

type StepInputProps = {
  interventionId: number;
  step: MobileStep;
  value: StepValue;
  onChange: (v: StepValue) => void;
  brandColor: string;
};

function DisplayBox({
  text,
  unit,
  emphasis = "normal",
}: Readonly<{ text: string; unit?: string | null; emphasis?: "callout" | "normal" }>) {
  // Callout ambre saillant (miroir PWA .info-callout-cerfa) : icone + texte
  // gras contraste pour que le tech ne puisse pas rater le prerequis a preparer.
  if (emphasis === "callout") {
    return (
      <View style={styles.calloutBox}>
        <Text style={styles.calloutIcon}>📋</Text>
        <Text style={styles.calloutText}>{text}</Text>
      </View>
    );
  }
  return (
    <View style={styles.displayBox}>
      <Text style={styles.displayBoxText}>{text}</Text>
      {unit ? <Text style={styles.unitLabel}>{unit}</Text> : null}
    </View>
  );
}

function normalizeBooleanValue(value: StepValue): "OUI" | "NON" | "" {
  const current = (typeof value === "string" ? value : "").toLowerCase();
  if (current === "1" || current === "true" || current === "oui") return "OUI";
  if (current === "0" || current === "false" || current === "non") return "NON";
  return "";
}

function ChoicePill({
  label,
  isActive,
  brandColor,
  onPress,
}: Readonly<{
  label: string;
  isActive: boolean;
  brandColor: string;
  onPress: () => void;
}>) {
  return (
    <Pressable
      style={[
        styles.choiceOption,
        isActive && { backgroundColor: brandColor, borderColor: brandColor },
      ]}
      onPress={onPress}
    >
      <View style={[styles.choiceDot, isActive && styles.choiceDotActive]}>
        {isActive ? <View style={styles.choiceDotInner} /> : null}
      </View>
      <Text style={[styles.choiceOptionText, isActive && styles.choiceOptionTextActive]}>
        {label}
      </Text>
    </Pressable>
  );
}

// Devis viewer : une ligne tappable par devis (id + numero + statut + bouton
// "Voir le devis"). Au tap -> telechargement binaire + ouverture native
// (expo-sharing). Retour explicite en cas d'echec (l'utilisateur a clique).
function DevisListDisplay({ devis }: Readonly<{ devis: readonly DevisItem[] }>) {
  const [openingId, setOpeningId] = useState<number | null>(null);

  const handleOpen = useCallback(async (devisId: number) => {
    setOpeningId(devisId);
    try {
      await openDevisPdf(devisId);
    } catch (error) {
      Alert.alert(
        "Devis",
        error instanceof Error ? error.message : "Impossible d'ouvrir le devis."
      );
    } finally {
      setOpeningId(null);
    }
  }, []);

  return (
    <View style={styles.displayBox}>
      {devis.map((d) => (
        <View key={d.id} style={styles.devisRow}>
          <Text style={styles.devisRowLabel}>
            {`#${d.id} – ${d.numero_devis || "Sans numéro"} (${d.statut || "-"})`}
          </Text>
          <Pressable
            style={styles.devisRowButton}
            onPress={() => handleOpen(d.id)}
            disabled={openingId !== null}
            accessibilityRole="button"
            accessibilityLabel={`Voir le devis ${d.numero_devis || d.id}`}
          >
            {openingId === d.id ? (
              <ActivityIndicator size="small" color="#ffffff" />
            ) : (
              <Text style={styles.devisRowButtonText}>👁 Voir le devis</Text>
            )}
          </Pressable>
        </View>
      ))}
    </View>
  );
}

function DisplayStepInput({ step }: Readonly<{ step: MobileStep }>) {
  // Priorite : message auto serveur (applyPhase1CerfaInfoOverride : "CERFA
  // obligatoire a preparer.", etc.) > display_content resolu serveur (option
  // backend pour "LISTE DEVIS *") > resolution client des devis (fallback) >
  // note > label.
  const meta = getStepMetadata(step);
  const devisList = useContext(DevisListContext);
  // Devis viewer : sans override serveur (auto_message/display_content, qui
  // restent du texte brut prioritaire), une etape "LISTE DEVIS *" avec des
  // devis a afficher est rendue en LIGNES TAPPABLES plutot qu'en bloc texte.
  const devisRows =
    !meta.auto_message && !meta.display_content
      ? selectDevisForStep(step.label, devisList)
      : null;
  if (devisRows && devisRows.length > 0) {
    return <DevisListDisplay devis={devisRows} />;
  }
  const devisText = resolveDevisStepText(step.label, devisList);
  const displayText = String(
    meta.auto_message || meta.display_content || devisText || meta.note || step.label || ""
  );
  // Phase 1 CERFA / prelevement huile : le serveur marque les vrais prerequis
  // a preparer avec auto_message_emphasis="callout" -> rendu ambre saillant.
  // "normal"/null -> box neutre discrete (cas "Aucun ... a preparer").
  const emphasis = meta.auto_message_emphasis === "callout" ? "callout" : "normal";
  return <DisplayBox text={displayText} emphasis={emphasis} />;
}

function AutoCalcStepInput({ step }: Readonly<{ step: MobileStep }>) {
  const computed = step.value;
  let display: string;
  if (computed === null || computed === undefined || computed === "") {
    display = "En attente des relevés...";
  } else if (typeof computed === "object") {
    display = JSON.stringify(computed);
  } else {
    display = String(computed);
  }
  return <DisplayBox text={display} unit={step.unit} />;
}

function NumberStepInput({
  step,
  value,
  onChange,
}: Readonly<{ step: MobileStep; value: StepValue; onChange: (v: StepValue) => void }>) {
  const numericText =
    typeof value === "number" || typeof value === "string" ? String(value) : "";
  return (
    <View style={styles.inputRow}>
      <TextInput
        style={styles.textInput}
        keyboardType="numeric"
        value={numericText}
        onChangeText={onChange}
        placeholder={step.unit ? `Valeur (${step.unit})` : "0"}
        placeholderTextColor="#94a3b8"
      />
      {step.unit ? <Text style={styles.unitLabel}>{step.unit}</Text> : null}
    </View>
  );
}

function TextStepInput({
  value,
  onChange,
}: Readonly<{ value: StepValue; onChange: (v: StepValue) => void }>) {
  const textValue = typeof value === "string" ? value : "";
  return (
    <TextInput
      style={styles.textInput}
      value={textValue}
      onChangeText={onChange}
      placeholder="Saisir..."
      placeholderTextColor="#94a3b8"
    />
  );
}

function CerfaStepInput({
  value,
  onChange,
  brandColor,
}: Readonly<{ value: StepValue; onChange: (v: StepValue) => void; brandColor: string }>) {
  const cerfaValue =
    value && typeof value === "object" && !Array.isArray(value)
      ? (value as { enabled?: boolean; reference?: string })
      : { enabled: false, reference: "" };
  const enabled = Boolean(cerfaValue.enabled);
  const reference = String(cerfaValue.reference ?? "");

  return (
    <View style={styles.cerfaBlock}>
      <Pressable
        style={[
          styles.cerfaToggle,
          enabled && { backgroundColor: brandColor, borderColor: brandColor },
        ]}
        onPress={() => onChange({ enabled: !enabled, reference } as unknown as StepValue)}
      >
        <View style={[styles.choiceDot, enabled && styles.choiceDotActive]}>
          {enabled ? <View style={styles.choiceDotInner} /> : null}
        </View>
        <Text style={[styles.cerfaToggleText, enabled && styles.choiceOptionTextActive]}>
          Oui, preparer
        </Text>
      </Pressable>
      {enabled ? (
        <TextInput
          style={styles.textInput}
          value={reference}
          onChangeText={(text) =>
            onChange({ enabled: true, reference: text } as unknown as StepValue)
          }
          placeholder="Saisir le numero..."
          placeholderTextColor="#94a3b8"
        />
      ) : null}
    </View>
  );
}

function BooleanStepInput({
  value,
  onChange,
  brandColor,
}: Readonly<{ value: StepValue; onChange: (v: StepValue) => void; brandColor: string }>) {
  const norm = normalizeBooleanValue(value);
  return (
    <View style={styles.choiceGroup}>
      <ChoicePill
        label="OUI"
        isActive={norm === "OUI"}
        brandColor={brandColor}
        onPress={() => onChange("1")}
      />
      <ChoicePill
        label="NON"
        isActive={norm === "NON"}
        brandColor={brandColor}
        onPress={() => onChange("0")}
      />
    </View>
  );
}

function ChoiceStepInput({
  step,
  value,
  onChange,
  brandColor,
}: Readonly<{
  step: MobileStep;
  value: StepValue;
  onChange: (v: StepValue) => void;
  brandColor: string;
}>) {
  const options = Array.isArray(step.options) ? step.options : [];
  if (options.length === 0) {
    return <DisplayBox text="Aucune option disponible." />;
  }
  const selected = typeof value === "string" ? value : "";
  return (
    <View style={styles.choiceGroup}>
      {options.map((opt) => (
        <ChoicePill
          key={opt}
          label={opt}
          isActive={selected === opt}
          brandColor={brandColor}
          onPress={() => onChange(opt)}
        />
      ))}
    </View>
  );
}

function getWorkflowStepId(step: MobileStep): number | null {
  const m = (step.metadata || {}) as { workflow_step_id?: number | null };
  return typeof m.workflow_step_id === "number" ? m.workflow_step_id : null;
}

function PhotoStepInput({ interventionId, step, value, onChange, brandColor }: Readonly<StepInputProps>) {
  const photos = isPhotoArrayValue(value) ? value : [];
  return (
    <View style={styles.photoBlock}>
      <CameraCapture
        interventionId={interventionId}
        stepId={step.id}
        workflowStepId={getWorkflowStepId(step)}
        brandColor={brandColor}
        onCaptured={(uri) => {
          const nextPhoto: StepPhotoValue = {
            id: `${Date.now()}`,
            uri,
            name: uri.split("/").pop(),
            mime_type: uri.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg",
            uploaded: false,
          };
          onChange([...photos, nextPhoto]);
        }}
      />
      {photos.map((photo) => (
        <View key={photo.id} style={styles.photoRow}>
          <Image source={{ uri: resolveMediaFileUri(photo.uri) }} style={styles.photoPreview} />
          <View style={styles.photoMeta}>
            <Text style={styles.photoName}>
              {photo.name || photo.uri.split("/").pop() || "photo"}
            </Text>
            <Text style={styles.photoHint}>{photo.uploaded ? "Synchronisee" : "Locale"}</Text>
          </View>
          <Pressable
            style={styles.removePhotoButton}
            onPress={() => onChange(photos.filter((item) => item.id !== photo.id))}
          >
            <Text style={styles.removePhotoText}>Supprimer</Text>
          </Pressable>
        </View>
      ))}
    </View>
  );
}

function SignatureStepInput({ interventionId, step, value, onChange, brandColor }: Readonly<StepInputProps>) {
  const signature = isSignatureValue(value) ? value : null;
  return (
    <View style={styles.signatureBlock}>
      <SignatureCapture
        interventionId={interventionId}
        stepId={step.id}
        brandColor={brandColor}
        onCaptured={(uri) =>
          onChange({
            signed: true,
            signed_by: "technicien",
            uri,
            name: uri.split("/").pop(),
            mime_type: "image/png",
          })
        }
      />
      {signature?.uri ? (
        <View style={styles.signaturePreviewWrap}>
          <Image source={{ uri: resolveMediaFileUri(signature.uri) }} style={styles.signaturePreview} resizeMode="contain" />
          <Pressable style={styles.removePhotoButton} onPress={() => onChange(null)}>
            <Text style={styles.removePhotoText}>Effacer signature</Text>
          </Pressable>
        </View>
      ) : null}
    </View>
  );
}

function UnsupportedStepInput() {
  return (
    <View style={styles.unsupportedBlock}>
      <Text style={styles.mutedText}>Type d'etape non pris en charge</Text>
    </View>
  );
}

function isDisplayKind(kind: string, step: MobileStep): boolean {
  return (
    kind === "display" ||
    kind === "section" ||
    kind === "info" ||
    isDisplayOnly(step)
  );
}

function isPhotoKind(kind: string, step: MobileStep): boolean {
  return kind === "photo" || step.type === "photo";
}

function isTextKind(kind: string, step: MobileStep): boolean {
  return kind === "text" || (kind === "prerequisite" && !isCerfaLike(step));
}

function StepInput(props: Readonly<StepInputProps>) {
  // Dispatch sur input_kind (valeur serveur brute) miroir de renderStepBlock
  // dans public/intervention_etapes.php — pour ne pas inventer de nouveaux
  // comportements. step.type est une "hint" qui compresse choice/boolean
  // en checklist, on prefere le kind serveur pour distinguer.
  const { step } = props;
  const meta = (step.metadata || {}) as { input_kind?: string };
  const kind = String(meta.input_kind || "").toLowerCase();

  if (isDisplayKind(kind, step)) return <DisplayStepInput step={step} />;
  if (isAutoCalc(step)) return <AutoCalcStepInput step={step} />;
  if (kind === "number") return <NumberStepInput {...props} />;
  if (isTextKind(kind, step)) return <TextStepInput {...props} />;
  if (kind === "prerequisite" && isCerfaLike(step)) return <CerfaStepInput {...props} />;
  if (kind === "boolean") return <BooleanStepInput {...props} />;
  if (kind === "choice") return <ChoiceStepInput {...props} />;
  if (isPhotoKind(kind, step)) return <PhotoStepInput {...props} />;
  if (step.type === "signature") return <SignatureStepInput {...props} />;
  return <UnsupportedStepInput />;
}

const styles = StyleSheet.create({
  root: { flex: 1, backgroundColor: "#edf2f8" },
  scroll: { flex: 1 },
  content: { padding: 16, gap: 12, paddingBottom: 16 },
  pageTitle: { fontSize: 18, fontWeight: "700", color: "#16325c" },
  phaseStrip: {
    backgroundColor: "#ffffff",
    borderWidth: 1,
    borderRadius: 10,
    padding: 12,
    gap: 4,
  },
  phaseStripCounter: { fontSize: 12, fontWeight: "700", textTransform: "uppercase" },
  phaseStripName: { fontSize: 16, fontWeight: "600", color: "#1f2f4f" },
  phaseAlert: {
    backgroundColor: "#fef2f2",
    borderWidth: 1,
    borderColor: "#fca5a5",
    color: "#b91c1c",
    borderRadius: 8,
    padding: 10,
    fontSize: 13,
  },
  loaderRow: { flexDirection: "row", alignItems: "center", gap: 8 },
  mutedText: { color: "#6a7a96" },
  stepCard: { backgroundColor: "#ffffff", borderRadius: 12, borderWidth: 1, borderColor: "#d8e4f6", overflow: "hidden" },
  stepHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", padding: 12, gap: 8 },
  stepMeta: { flex: 1, flexDirection: "row", alignItems: "center", gap: 10 },
  stepIndex: { width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" },
  stepIndexText: { color: "#ffffff", fontSize: 12, fontWeight: "700" },
  stepInfo: { flex: 1, gap: 4 },
  stepLabel: { fontSize: 14, fontWeight: "600", color: "#1f2f4f" },
  requiredMark: { color: "#dc2626" },
  statusBadge: { alignSelf: "flex-start", borderWidth: 1, borderRadius: 999, paddingVertical: 2, paddingHorizontal: 7 },
  statusBadgeText: { fontSize: 11, fontWeight: "700" },
  chevron: { color: "#94a3b8", fontSize: 12 },
  stepBody: { padding: 12, borderTopWidth: 1, borderTopColor: "#e3ebf8", gap: 10 },
  inputRow: { flexDirection: "row", alignItems: "center", gap: 8 },
  textInput: { flex: 1, borderWidth: 1, borderColor: "#d8e4f6", borderRadius: 8, padding: 10, fontSize: 14, color: "#1f2f4f", backgroundColor: "#f8fbff" },
  textArea: { minHeight: 80, textAlignVertical: "top" },
  unitLabel: { fontSize: 13, color: "#66748f", fontWeight: "600" },
  checklistRow: { flexDirection: "row", gap: 10 },
  checkOption: { flex: 1, alignItems: "center", paddingVertical: 10, borderRadius: 8, borderWidth: 1, borderColor: "#d8e4f6", backgroundColor: "#f8fbff" },
  checkOptionText: { fontSize: 14, fontWeight: "600", color: "#334155" },
  checkOptionTextActive: { color: "#ffffff" },
  choiceGroup: { gap: 8 },
  choiceOption: {
    flexDirection: "row",
    alignItems: "center",
    gap: 10,
    paddingVertical: 10,
    paddingHorizontal: 12,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: "#d8e4f6",
    backgroundColor: "#f8fbff",
  },
  choiceOptionText: { flex: 1, fontSize: 14, fontWeight: "600", color: "#334155" },
  choiceOptionTextActive: { color: "#ffffff" },
  choiceDot: {
    width: 18,
    height: 18,
    borderRadius: 9,
    borderWidth: 2,
    borderColor: "#94a3b8",
    alignItems: "center",
    justifyContent: "center",
  },
  choiceDotActive: { borderColor: "#ffffff" },
  choiceDotInner: { width: 8, height: 8, borderRadius: 4, backgroundColor: "#ffffff" },
  displayBox: {
    padding: 12,
    backgroundColor: "#f1f5f9",
    borderRadius: 8,
    borderWidth: 1,
    borderColor: "#e2e8f0",
  },
  displayBoxText: { color: "#475569", fontSize: 13 },
  // Devis viewer — une ligne par devis : libelle a gauche, bouton "Voir" a droite.
  devisRow: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 10,
    paddingVertical: 6,
  },
  devisRowLabel: { flex: 1, color: "#475569", fontSize: 13 },
  devisRowButton: {
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 8,
    backgroundColor: "#1D4ED8",
    minWidth: 110,
    alignItems: "center",
    justifyContent: "center",
  },
  devisRowButtonText: { color: "#ffffff", fontSize: 13, fontWeight: "600" },
  // Callout ambre saillant — equivalent natif du .info-callout-cerfa PWA :
  // bordure gauche epaisse ambre, fond ambre clair, texte brun gras, icone.
  calloutBox: {
    flexDirection: "row",
    alignItems: "flex-start",
    gap: 10,
    padding: 14,
    backgroundColor: "#fef3c7",
    borderWidth: 1,
    borderColor: "#fcd34d",
    borderLeftWidth: 6,
    borderLeftColor: "#f59e0b",
    borderRadius: 10,
  },
  calloutIcon: { fontSize: 22, lineHeight: 26 },
  calloutText: { flex: 1, color: "#78350f", fontSize: 15, fontWeight: "600", lineHeight: 21 },
  cerfaBlock: { gap: 10 },
  cerfaToggle: {
    flexDirection: "row",
    alignItems: "center",
    gap: 10,
    paddingVertical: 10,
    paddingHorizontal: 12,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: "#d8e4f6",
    backgroundColor: "#f8fbff",
  },
  cerfaToggleText: { fontSize: 14, fontWeight: "600", color: "#334155" },
  // Sous-etape (extension) : indentation, fond legerement different, barre
  // verticale a gauche pour rendre la hierarchie evidente.
  stepCardExtension: {
    marginLeft: 20,
    borderLeftWidth: 4,
    borderLeftColor: "#fbbf24",
    backgroundColor: "#fffbeb",
  },
  extensionBadge: {
    alignSelf: "flex-start",
    paddingHorizontal: 8,
    paddingVertical: 3,
    borderTopLeftRadius: 12,
    borderBottomRightRadius: 8,
  },
  extensionBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "700" },
  // Bloc commentaire qui s'affiche sous une etape (requires_comment ou
  // extension choice + valeur probleme/panne).
  commentWrap: { gap: 6 },
  commentLabel: { fontSize: 13, fontWeight: "600", color: "#334155" },
  unsupportedBlock: { padding: 12, backgroundColor: "#f1f5f9", borderRadius: 8, alignItems: "center" },
  photoBlock: { gap: 8 },
  photoRow: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
    borderWidth: 1,
    borderColor: "#d8e4f6",
    borderRadius: 8,
    padding: 8,
    backgroundColor: "#f8fbff",
  },
  photoPreview: {
    width: 64,
    height: 64,
    borderRadius: 6,
    backgroundColor: "#dbe7f8",
  },
  photoMeta: { flex: 1, gap: 2 },
  photoName: { color: "#1f2f4f", fontSize: 13, fontWeight: "600" },
  photoHint: { color: "#64748b", fontSize: 12 },
  removePhotoButton: {
    backgroundColor: "#fee2e2",
    borderRadius: 8,
    paddingVertical: 6,
    paddingHorizontal: 10,
  },
  removePhotoText: { color: "#b91c1c", fontWeight: "700", fontSize: 12 },
  signatureBlock: { gap: 8 },
  signaturePreviewWrap: {
    borderWidth: 1,
    borderColor: "#d8e4f6",
    borderRadius: 8,
    padding: 8,
    gap: 8,
    backgroundColor: "#f8fbff",
  },
  signaturePreview: {
    width: "100%",
    height: 120,
    backgroundColor: "#ffffff",
    borderRadius: 6,
  },
  validationError: { color: "#dc2626", fontSize: 12 },
  completeButton: { borderRadius: 8, paddingVertical: 10, alignItems: "center" },
  completeButtonText: { color: "#ffffff", fontWeight: "700" },
  finishError: { color: "#dc2626", fontSize: 13 },
  actionsBar: { flexDirection: "row", gap: 10, padding: 16, backgroundColor: "#ffffff", borderTopWidth: 1, borderTopColor: "#d8e4f6" },
  backButton: { flex: 1, backgroundColor: "#e5e7eb", borderRadius: 10, alignItems: "center", paddingVertical: 12 },
  backButtonText: { color: "#334155", fontWeight: "700" },
  finishButton: { flex: 2, borderRadius: 10, alignItems: "center", paddingVertical: 12 },
  finishButtonText: { color: "#ffffff", fontWeight: "700", textAlign: "center" },
  // Photo machine overlay
  machinePhotoOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, zIndex: 90, backgroundColor: "#edf2f8", padding: 16 },
  machinePhotoHeader: { marginBottom: 16 },
  machinePhotoTitle: { fontSize: 20, fontWeight: "700", color: "#16325c", marginBottom: 8 },
  machinePhotoSubtitle: { fontSize: 14, color: "#6b7280", lineHeight: 20 },
  machinePhotoLoading: { flex: 1, alignItems: "center", justifyContent: "center" },
  machinePhotoLoadingOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, alignItems: "center", justifyContent: "center", backgroundColor: "rgba(0,0,0,0.45)", borderRadius: 12 },
  machinePhotoSkip: { alignSelf: "center", marginTop: 16, paddingVertical: 10, paddingHorizontal: 24, backgroundColor: "#e5e7eb", borderRadius: 8 },
  machinePhotoSkipText: { color: "#334155", fontWeight: "600", fontSize: 15 },
});
