import { LAST_BATCH_LS } from '../config/constants.js';
import { api } from '../core/api.js';
import { $, ensureEditorDom } from '../core/dom.js';
import { readLs, writeLs } from '../core/storage.js';
import { scheduleUserPreferencesSave, getLastBatchImagesMap, rememberLastBatchImage } from './preferences.js';
import { getConfiguredFieldsForActiveDocType, getCurrentProjectKind, getVisibleFieldBases, installWorkEditableBases, isActiveWorkNonDataDocType, isBdmMixedDocTypeActive, isCoverpageDocType, isHeaderKeyOnlyDocType, isHiddenField, isNonDataDocType, normalizeDocTypeKey, resolveFieldConfigRootKind, resolveFieldLabel, sanitizeWorkPayloadForConfiguredFields } from './project-context.js';
import { hideSuggestions, primeOrigInputs } from './suggestions.js';
import { docTypeState, state, runtime } from '../state/app-state.js';
import { flushAutosaveBeforeNavigation, pushHistoryAction, resetAutosaveState, resetHistoryState } from '../state/autosave.js';
import { clearWorkMemoryCache, clearWorkPrefetchCache, consumePrefetchedWork, ensureImageListContextMenu, formContent, getAssignableSubProjects, getCachedWorkPayload, getCurrentBatchProjectInfo, hideDocTypeChangeOverlay, imageListContextMenu, invalidateCachedWorkPayload, prefetchNeighborWork, recordsBlock, resetWorkspaceAfterClose, setCachedWorkPayload, setDocTypeOptions, showDocTypeChangeOverlay } from '../ui/events.js';
import { applyEmptyFilter, applyFieldTableColumnWidths, buildFieldTable, renderFieldRow, renderHeaderFormTypeRow, renderRecordsBlock } from '../ui/form/field-table.js';
import { fitImageToViewAfterLoad, fitImageToViewPreservingZoom, focusFirstHeaderOrigInput, hideOrigSnippets } from '../ui/highlight.js';
import { applyBdmViewZoom, applyDeferredWorkDetection, applyImageViewerTab, isWorkBdmImageView, renderActRectsOverlay, updateBdmToolbarVisibility } from '../ui/image-viewer.js';
import { highlightList, renderImageList } from '../ui/layout.js';
import { maybePromptMixedDocTypeOnLoad } from './mixed-doctype-prompt.js';
import { showConfirmActionPanel, showLotAssignmentPanel } from '../ui/modals.js';
import {
  clearSessionChefPassword,
  isChefPasswordAuthError,
  isProjectKindsProtected,
  promptChefPasswordForAction,
  refreshProjectKindsLockStatus,
  withChefPassword,
} from '../ui/settings/project-kinds-lock.js';
import { closeSubProjectAssignPanel, loadProjectKinds, loadSubProjects, syncFieldsKindTabToCurrentProject } from '../ui/settings/panel.js';
import { setStatus, updateVisibleStatusBar } from '../ui/status-bar.js';

export async function refreshCurrentWorkView() {
  if (state.imageIndex < 0 || state.imageIndex >= state.images.length) return;
  const current = state.images[state.imageIndex];
  if (!current) return;
  invalidateCachedWorkPayload(state.batch, current.image_id);
  await loadWork(current.image_id);
}


export function setDocTypeLoading(isLoading) {
  const loading = !!isLoading;
  const sel = $("docTypeSelect");
  const wrapper = sel ? sel.closest(".topbar__doctype") : null;
  if (wrapper) wrapper.classList.toggle("topbar__doctype--loading", loading);
  if (sel) {
    sel.disabled = loading;
    sel.setAttribute("aria-busy", loading ? "true" : "false");
  }
}


export async function updateCurrentImageDocType(nextDocType, options) {
  const opts = options || {};
  const requireConfirm = opts.requireConfirm !== false;
  const pushToHistory = opts.pushToHistory !== false;
  const selected = String(nextDocType || "").trim();
  const sel = $("docTypeSelect");
  const previous = String((state.work && state.work.form_type) || state.docType || "").trim();
  if (!selected || selected === previous) {
    state.docType = selected || previous;
    return;
  }
  if (!state.batch || !state.work || state.work.image_id == null) {
    state.docType = selected;
    return;
  }
  if (docTypeState.inFlight) return;

  if (requireConfirm) {
    // Leaving Non-Data never deletes records; skip confirm for Non-Data → Non-Data
    // and Non-Data → Data (including header-only types such as Coverpage).
    const previousIsNonData = isNonDataDocType(previous);
    if (!previousIsNonData) {
      const currentLabel = previous || "(vide)";
      const confirmMsg =
        "Actuel : " + currentLabel + "\n" +
        "Nouveau : " + selected + "\n\n" +
        "Continuer ?";
      const ok = await showConfirmActionPanel(
        "Changer le DocType",
        confirmMsg,
        "OK",
        {
          cancelLabel: "Annuler",
          kind: "default",
          warningText: "Vous êtes sur le point de changer le DocType pour cette image.",
        }
      );
      if (!ok) {
        if (sel) sel.value = previous || selected;
        state.docType = previous || selected;
        return;
      }
    }
  }

  if (!(await flushAutosaveBeforeNavigation("doctype-change"))) {
    if (sel && previous) sel.value = previous;
    state.docType = previous;
    return;
  }

  docTypeState.inFlight = true;
  setDocTypeLoading(true);
  showDocTypeChangeOverlay();
  setStatus("Mise à jour du DocType...");
  try {
    const allowedRecordFields = getConfiguredFieldsForActiveDocType(
      getCurrentProjectKind(),
      selected
    )
      .map((f) => String((f && f.fieldName) || "").trim())
      .filter(Boolean);
    const censusHeaderFields = getConfiguredFieldsForActiveDocType(
      getCurrentProjectKind(),
      "Census"
    )
      .map((f) => String((f && f.fieldName) || "").trim())
      .filter(Boolean);
    const coverpageHeaderFields = getConfiguredFieldsForActiveDocType(
      getCurrentProjectKind(),
      "Coverpage"
    )
      .map((f) => String((f && f.fieldName) || "").trim())
      .filter(Boolean);
    await api(`/api/batch/${encodeURIComponent(state.batch)}/image/${state.work.image_id}/doctype`, {
      method: "POST",
      body: JSON.stringify({
        doc_type: selected,
        allowed_record_fields: allowedRecordFields,
        census_header_fields: censusHeaderFields,
        coverpage_header_fields: coverpageHeaderFields,
      }),
    });
    if (pushToHistory) {
      pushHistoryAction({
        kind: "doc-type-change",
        imageId: state.work.image_id,
        oldDocType: previous,
        newDocType: selected,
        timestamp: Date.now(),
      });
    }
    state.docType = selected;
    if (state.work) state.work.form_type = selected;
    await refreshCurrentWorkView();
    setStatus("DocType mis à jour");
  } catch (e) {
    state.docType = previous;
    if (state.work) state.work.form_type = previous;
    if (sel) {
      if (previous && !Array.from(sel.options).some((o) => o.value === previous)) {
        const o = document.createElement("option");
        o.value = previous;
        o.textContent = previous;
        sel.appendChild(o);
      }
      sel.value = previous;
    }
    setStatus(e.message, true);
  } finally {
    docTypeState.inFlight = false;
    setDocTypeLoading(false);
    hideDocTypeChangeOverlay();
  }
}


export async function saveBatchSubProjectAssignment(batchName, subProjectName, chefPassword) {
  return api("/api/project-kinds/assign", {
    method: "POST",
    body: JSON.stringify(
      withChefPassword(
        {
          batch_name: String(batchName || "").trim(),
          sub_project: String(subProjectName || "").trim(),
        },
        chefPassword
      )
    ),
  });
}


export async function applySubProjectAssignmentFromPanel(applyToAllLots) {
  const selection = runtime.subProjectAssignSelection;
  const selectEl = $("subProjectAssignSelect");
  const selected = selectEl ? String(selectEl.value || "").trim() : "";
  if (!selection || !selection.lotName || !selected) {
    closeSubProjectAssignPanel();
    return;
  }
  try {
    if (applyToAllLots) {
      const lots = (Array.isArray(state.projectKinds) ? state.projectKinds : [])
        .map((p) => String((p && p.project_name) || "").trim())
        .filter(Boolean);
      const lotCount = lots.length;
      const lotDetails = lotCount <= 10 && lotCount > 0
        ? `\n\nLots concernés :\n• ${lots.join("\n• ")}`
        : "";
      const confirmed = await showConfirmActionPanel(
        "Appliquer le sous-projet à tous les lots ?",
        `Le sous-projet « ${selected} » sera appliqué à ${lotCount} lot${lotCount === 1 ? "" : "s"}.`
          + lotDetails
          + "\n\nCette action remplacera l’affectation actuelle de chaque lot concerné.",
        "Appliquer à tous les lots",
        { cancelLabel: "Annuler", kind: "danger" }
      );
      if (!confirmed) return;

      let chefPassword = selection.pendingChefPassword;
      if (chefPassword == null) {
        chefPassword = await promptChefPasswordForAction(
          "Saisissez le mot de passe Chef de projet pour appliquer ce sous-projet à tous les lots."
        );
        if (chefPassword === null) return;
      }

      setStatus("Enregistrement du sous-projet pour tous les lots détectés...");
      let ok = 0;
      let failed = 0;
      for (let i = 0; i < lots.length; i += 1) {
        try {
          await saveBatchSubProjectAssignment(lots[i], selected, chefPassword);
          ok += 1;
        } catch (_) {
          failed += 1;
        }
      }
      closeSubProjectAssignPanel();
      await loadProjectKinds();
      syncFieldsKindTabToCurrentProject();
      setStatus(
        failed
          ? `Sous-projet appliqué à ${ok} lot(s). Échec sur ${failed} lot(s).`
          : `Sous-projet appliqué à ${ok} lot(s).`
      );
      return;
    }
    let chefPassword = selection.pendingChefPassword;
    if (chefPassword == null) {
      chefPassword = await promptChefPasswordForAction(
        "Saisissez le mot de passe Chef de projet pour modifier l'affectation du lot."
      );
      if (chefPassword === null) return;
    }
    setStatus("Enregistrement du sous-projet...");
    await saveBatchSubProjectAssignment(selection.lotName, selected, chefPassword);
    closeSubProjectAssignPanel();
    await loadProjectKinds();
    syncFieldsKindTabToCurrentProject();
    setStatus("Sous-projet du lot enregistré.");
  } catch (e) {
    setStatus(String(e && e.message ? e.message : e), true);
  }
}


export async function maybePromptBatchSubProjectAssignment() {
  const batchName = String(state.batch || "").trim();
  if (!batchName) return true;
  const project = getCurrentBatchProjectInfo();
  if (!project) return true;

  const mandatory = !!project.needs_sub_project;
  if (!mandatory && !state.showLotAssignmentPrompt) return true;

  try {
    await refreshProjectKindsLockStatus();
  } catch (_) {
    // Keep prior lock state if the status endpoint is briefly unavailable.
  }

  const rootKind = String(project.kind || "");
  const options = mandatory
    ? (state.subProjects || [])
        .map((sp) => String(sp.name || "").trim())
        .filter(Boolean)
    : getAssignableSubProjects(rootKind);
  const result = await showLotAssignmentPanel({
    batchName,
    rootKind,
    suggestedSubProject: project.auto_sub_project || project.sub_project || "",
    candidateSubProjects: project.candidate_sub_projects || [],
    options,
    mandatory,
    needsBootstrap: !!project.needs_bootstrap,
  });

  if (!result || result.action === "skip") {
    return !mandatory;
  }
  try {
    const selected = String(result.subProject || "").trim();
    if (!selected) return !mandatory;
    const passwordMessage = isProjectKindsProtected()
      ? "Saisissez le mot de passe Chef de projet pour attribuer le sous-projet à ce lot."
      : undefined;
    let chefPassword = await promptChefPasswordForAction(passwordMessage, {
      reuseSession: true,
    });
    if (chefPassword === null) return !mandatory;
    try {
      await saveBatchSubProjectAssignment(batchName, selected, chefPassword);
    } catch (assignErr) {
      // Stale/wrong session password: clear and ask once more in this flow.
      if (!isProjectKindsProtected() || !isChefPasswordAuthError(assignErr)) {
        throw assignErr;
      }
      clearSessionChefPassword();
      chefPassword = await promptChefPasswordForAction(passwordMessage, {
        reuseSession: true,
      });
      if (chefPassword === null) return !mandatory;
      await saveBatchSubProjectAssignment(batchName, selected, chefPassword);
    }
    await loadProjectKinds();
    syncFieldsKindTabToCurrentProject();
    setStatus(
      project.needs_bootstrap
        ? "Sous-projet enregistr\u00e9 et base de sortie cr\u00e9\u00e9e."
        : "Sous-projet du lot enregistr\u00e9."
    );
    return true;
  } catch (e) {
    setStatus(String(e && e.message ? e.message : e), true);
    return false;
  }
}


export async function loadSettings() {
  const s = await api("/api/settings");
  $("projectRoot").value = s.project_root || "";
  updateVisibleStatusBar();
}


export async function applyRootFromInput() {
  const project_root = $("projectRoot").value.trim();
  if (!project_root) return;
  setStatus("Enregistrement du chemin...");
  try {
    await api("/api/settings", {
      method: "POST",
      body: JSON.stringify({ project_root }),
    });
    setStatus("Dossier enregistr\u00e9");
    await loadBatches();
  } catch (e) {
    setStatus(e.message, true);
  }
}


export async function closeFolder() {
  if (!(await flushAutosaveBeforeNavigation("close-folder"))) return;
  setStatus("Fermeture du dossier...");
  try {
    await api("/api/settings/close", { method: "POST" });
    resetWorkspaceAfterClose();
    setStatus("Dossier ferm\u00e9");
  } catch (e) {
    setStatus(e.message, true);
  }
}


export async function openFolder() {
  setStatus("Ouverture du sélecteur de dossier...");
  try {
    const r = await api("/api/pick-folder");
    if (r.cancelled) {
      const input = $("projectRoot");
      if (input) {
        input.focus();
        input.select();
      }
      setStatus("Saisissez le chemin du dossier puis appuyez sur Entrée.");
      return;
    }
    if (r.error) {
      setStatus(r.error, true);
      return;
    }
    const root = r.project_root;
    if (root) $("projectRoot").value = root;
    setStatus("Dossier enregistr\u00e9");
    await loadBatches();
  } catch (e) {
    setStatus(e.message, true);
  }
}


export async function loadBatches() {
  const { batches } = await api("/api/batches");
  const sel = $("batchSelect");
  const normalizedBatches = Array.from(
    new Set(
      (Array.isArray(batches) ? batches : [])
        .map((batchName) => String(batchName == null ? "" : batchName).trim())
        .filter((batchName) => batchName.length > 0)
    )
  );
  // Defensive reset for QWebEngine select popup glitches in desktop EXE mode.
  sel.multiple = false;
  sel.size = 1;
  sel.innerHTML = "";
  normalizedBatches.forEach((b) => {
    const o = document.createElement("option");
    o.value = b;
    o.textContent = b;
    sel.appendChild(o);
  });
  const storedBatch = (state.preferredLastBatch || readLs(LAST_BATCH_LS) || "").trim();
  const preferredBatch = String(state.batch || storedBatch || "").trim();

  if (preferredBatch && normalizedBatches.includes(preferredBatch)) {
    state.batch = preferredBatch;
    sel.value = preferredBatch;
  } else if (normalizedBatches.length) {
    state.batch = normalizedBatches[0];
    sel.value = state.batch;
    writeLs(LAST_BATCH_LS, state.batch);
    scheduleUserPreferencesSave({ last_batch: state.batch });
  } else {
    state.batch = "";
    writeLs(LAST_BATCH_LS, null);
    scheduleUserPreferencesSave({ last_batch: "" });
  }
  if (state.batch) {
    // Ensure sub-project field definitions are available before first image render.
    await loadSubProjects();
    await loadProjectKinds();
    await onBatchChange();
  } else {
    await loadSubProjects();
    await loadProjectKinds();
  }
}


export function applyDocTypeFromWork(formType) {
  if (!formType) return;
  const sel = $("docTypeSelect");
  if (!sel) return;
  const v = String(formType).trim();
  if (!v) return;
  if (!Array.from(sel.options).some((o) => o.value === v)) {
    const o = document.createElement("option");
    o.value = v;
    o.textContent = v;
    sel.appendChild(o);
  }
  sel.value = v;
  state.docType = v;
}


export async function loadDocTypesForBatch() {
  if (!state.batch) {
    setDocTypeOptions([]);
    return;
  }
  try {
    const r = await api(`/api/batch/${encodeURIComponent(state.batch)}/doctypes`);
    state.batchDocTypeGroups = r.doc_type_groups || null;
    setDocTypeOptions(r.doc_types || [], r.default_doc_type || "", r.doc_type_groups || null);
  } catch (_) {
    setDocTypeOptions([]);
  }
}


/**
 * Build a provisional image URL so the page can paint before /image-meta returns.
 * @param {string} batchName
 * @param {string|number} imageId
 * @returns {string}
 */
function provisionalImageUrl(batchName, imageId) {
  return (
    `/api/image?batch=${encodeURIComponent(String(batchName || ""))}` +
    `&id=${encodeURIComponent(String(imageId))}`
  );
}


/**
 * Prefer the cached/prefetched versioned URL (same key as neighbor image warm-up).
 * @param {string} batchName
 * @param {string|number} imageId
 * @returns {string}
 */
function resolveImmediateImageUrl(batchName, imageId) {
  const cached = getCachedWorkPayload(batchName, imageId);
  if (cached && cached.image_file_url) return String(cached.image_file_url);
  return provisionalImageUrl(batchName, imageId);
}


/**
 * True when two image URLs point at the same batch/image (ignores cache-buster `v`).
 * @param {string|null} left
 * @param {string|null} right
 * @returns {boolean}
 */
function isSameBatchImageUrl(left, right) {
  if (!left || !right) return false;
  if (left === right) return true;
  try {
    const a = new URL(left, window.location.origin);
    const b = new URL(right, window.location.origin);
    return (
      a.pathname === b.pathname &&
      a.searchParams.get("batch") === b.searchParams.get("batch") &&
      a.searchParams.get("id") === b.searchParams.get("id")
    );
  } catch (_) {
    return false;
  }
}


/**
 * Paint the target page image immediately during an image switch.
 * @param {string|null|undefined} batchName
 * @param {string|number} imageId
 * @returns {boolean}
 */
function paintImageForSwitch(batchName, imageId) {
  if (!batchName) return false;
  const pageImgEl = $("pageImg");
  const imgPanEl = $("imgPan");
  if (!pageImgEl) return false;
  runtime.pendingDisplayImageId = imageId;
  const actLayer = $("actRectsLayer");
  if (actLayer) {
    actLayer.innerHTML = "";
    actLayer.hidden = true;
  }
  return displayWorkImage(
    resolveImmediateImageUrl(batchName, imageId),
    imageId,
    pageImgEl,
    imgPanEl
  );
}


/**
 * Remove the previous image's headers/records from the form pane while the next loads.
 */
function clearFormPaneForImageSwitch() {
  hideOrigSnippets();
  hideSuggestions();
  const formEmpty = $("formEmpty");
  const formContentEl = $("formContent");
  if (formEmpty) formEmpty.hidden = false;
  if (formContentEl) formContentEl.hidden = true;
  const headerBlock = $("headerBlock");
  if (headerBlock) headerBlock.hidden = true;
  const addHeaderWrap = headerBlock
    ? headerBlock.querySelector(".header-add-section-wrap")
    : null;
  if (addHeaderWrap) addHeaderWrap.hidden = true;
  const bdmMixedAddWrap = $("bdmMixedAddWrap");
  if (bdmMixedAddWrap) bdmMixedAddWrap.hidden = true;
  const nonDataMessage = $("formNonDataMessage");
  if (nonDataMessage) nonDataMessage.hidden = true;
  const hf = $("headerFields");
  if (hf) hf.innerHTML = "";
  const rb = $("recordsBlock");
  if (rb) {
    rb.innerHTML = "";
    rb.hidden = true;
  }
  const countBar = $("formRecordCountBar");
  if (countBar) countBar.hidden = true;
  const countValue = $("formRecordCount");
  if (countValue) countValue.textContent = "0";
  const dictBtn = $("btnApplyDictionaryCorrections");
  if (dictBtn) dictBtn.disabled = true;
  const pageFillBtn = $("btnAiPageFill");
  if (pageFillBtn) pageFillBtn.disabled = true;
  const pageStructureBtn = $("btnAiPageStructure");
  if (pageStructureBtn) pageStructureBtn.disabled = true;
}


function displayWorkImage(imageUrl, imageId, pageImgEl, imgPanEl) {
  if (!imageUrl || !pageImgEl) return false;
  pageImgEl.onerror = function () {
    if (String(runtime.pendingDisplayImageId) !== String(imageId) &&
        !(state.work && String(state.work.image_id) === String(imageId))) {
      return;
    }
    if (state.imageViewerTab === "next") {
      setStatus("Impossible de charger l'image suivante.", true);
      if (imgPanEl) imgPanEl.hidden = true;
      $("noImg").textContent = "Image suivante introuvable.";
      $("noImg").hidden = false;
      return;
    }
    setStatus(
      "Impossible de charger l'image (v\u00e9rifiez ENTREES/" +
        state.batch +
        "/Images et le dossier projet).",
      true
    );
    if (imgPanEl) imgPanEl.hidden = true;
    $("noImg").hidden = false;
  };
  pageImgEl.onload = function () {
    requestAnimationFrame(function () {
      if (String(runtime.pendingDisplayImageId) !== String(imageId) &&
          !(state.work && String(state.work.image_id) === String(imageId))) {
        return;
      }
      if (state.imageViewerTab === "next") {
        fitImageToViewPreservingZoom();
        const layer = $("actRectsLayer");
        if (layer) {
          layer.innerHTML = "";
          layer.hidden = true;
        }
        return;
      }
      const isLoadedCurrentWork = !!(state.work && String(state.work.image_id) === String(imageId));
      // Fit as soon as the switched image is decoded — do not wait for /work.
      if (isLoadedCurrentWork && isWorkBdmImageView()) {
        applyBdmViewZoom();
      } else if (isLoadedCurrentWork && state.imageIndex <= 0 && state.zoom === 100) {
        fitImageToViewAfterLoad();
      } else {
        fitImageToViewPreservingZoom();
      }
      if (isLoadedCurrentWork) {
        renderActRectsOverlay();
        requestAnimationFrame(() => {
          focusFirstHeaderOrigInput();
        });
      }
    });
  };
  const currentSrc = pageImgEl.getAttribute("src");
  // Keep the provisional paint; upgrading only `v=` would reload the same file.
  if (!isSameBatchImageUrl(currentSrc, imageUrl)) {
    pageImgEl.src = imageUrl;
  } else if (pageImgEl.complete && pageImgEl.naturalWidth > 0) {
    // Already showing this image from an earlier paint — still fit the view.
    requestAnimationFrame(function () {
      if (state.imageViewerTab === "next") return;
      fitImageToViewPreservingZoom();
    });
  }
  if (imgPanEl) imgPanEl.hidden = false;
  $("noImg").hidden = true;
  return true;
}


export async function loadWork(imageId) {
  if (!state.batch) return;
  ensureEditorDom();
  const batchAtStart = state.batch;
  const loadToken = `${String(batchAtStart)}|${String(imageId)}|${Date.now()}|${Math.random()}`;
  runtime.activeWorkLoadToken = loadToken;
  setStatus("Affichage de l'image…");
  hideOrigSnippets();
  hideSuggestions();
  $("formEmpty").hidden = false;
  $("formContent").hidden = true;
  const dictBtnLoading = $("btnApplyDictionaryCorrections");
  if (dictBtnLoading) dictBtnLoading.disabled = true;
  const pageFillBtnLoading = $("btnAiPageFill");
  if (pageFillBtnLoading) pageFillBtnLoading.disabled = true;
  const pageStructureBtnLoading = $("btnAiPageStructure");
  if (pageStructureBtnLoading) pageStructureBtnLoading.disabled = true;

  const imgPanEl = $("imgPan");
  const pageImgEl = $("pageImg");
  // Re-assert image paint (may already be done in selectImageByIndex before autosave flush).
  paintImageForSwitch(batchAtStart, imageId);
  setStatus("Chargement headers / records…");
  runtime.nextImageWork = null;

  try {
    if (runtime.workDetectionController) {
      runtime.workDetectionController.abort();
      runtime.workDetectionController = null;
    }

    const imageMetaPromise = api(
      `/api/batch/${encodeURIComponent(batchAtStart)}/image-meta/${imageId}`
    )
      .then((meta) => {
        if (runtime.activeWorkLoadToken !== loadToken) return null;
        if (state.batch !== batchAtStart) return null;
        if (!meta || meta.image_id !== imageId || !meta.image_file_url) return null;
        displayWorkImage(meta.image_file_url, imageId, pageImgEl, imgPanEl);
        return meta;
      })
      .catch(() => null);

    let prefetchedWork = consumePrefetchedWork(state.batch, imageId);
    let w = getCachedWorkPayload(state.batch, imageId);
    if (!w && prefetchedWork) {
      try {
        w = await prefetchedWork;
      } catch (_) {
        w = null;
      }
    }
    if (!w) {
      w = await api(`/api/batch/${encodeURIComponent(state.batch)}/work/${imageId}`);
    }
    if (runtime.activeWorkLoadToken !== loadToken || state.batch !== batchAtStart) return;

    setStatus("Affichage des valeurs…");

    w = sanitizeWorkPayloadForConfiguredFields(w);
    installWorkEditableBases(w);
    setCachedWorkPayload(state.batch, imageId, w);
    state.work = w;
    applyDocTypeFromWork(w.form_type);

    $("imageTitle").textContent = "";
    updateVisibleStatusBar();
    updateBdmToolbarVisibility();

    if (w.image_file_url && pageImgEl) {
      displayWorkImage(w.image_file_url, imageId, pageImgEl, imgPanEl);

      if (w.bdm_detection_pending) {
        const ac = new AbortController();
        runtime.workDetectionController = ac;
        const iid = imageId;
        const batchName = state.batch;
        api(
          `/api/batch/${encodeURIComponent(batchName)}/work/${iid}/detection`,
          { signal: ac.signal }
        )
          .then((det) => {
            if (ac.signal.aborted) return;
            applyDeferredWorkDetection(det, iid);
          })
          .catch((err) => {
            if (err && err.name === "AbortError") return;
            console.warn("[BDM] detection deferred fetch failed:", err);
          });
      }
    } else {
      // No image URL - show helpful message
      const msg = "Les fichiers image sont manquants. V\u00e9rifiez que le dossier ENTREES/" + state.batch + "/Images/ existe et contient les images.";
      setStatus(msg, true);
      if (imgPanEl) imgPanEl.hidden = true;
      if (pageImgEl) pageImgEl.removeAttribute("src");
      renderActRectsOverlay();
      $("noImg").textContent = msg;
      $("noImg").hidden = false;
    }
    void imageMetaPromise;

    const headerBlock = $("headerBlock");
    const headerTitle = headerBlock ? headerBlock.querySelector("h2") : null;
    const addHeaderWrap = headerBlock ? headerBlock.querySelector(".header-add-section-wrap") : null;
    const hf = $("headerFields");
    if (addHeaderWrap && hf && addHeaderWrap.parentElement === hf) {
      headerBlock.appendChild(addHeaderWrap);
    } else if (addHeaderWrap && headerBlock && addHeaderWrap.parentElement !== headerBlock) {
      headerBlock.appendChild(addHeaderWrap);
    }
    hf.innerHTML = "";
    hf.className = "header-fields-root";
    const activeDocType = String((w && w.form_type) || state.docType || "");
    const isCensusMixed =
      resolveFieldConfigRootKind(getCurrentProjectKind()) === "Census"
      && normalizeDocTypeKey(activeDocType) === "mixed";
    const isBdmMixed = isBdmMixedDocTypeActive();
    const headerKeyOnlyMode = isHeaderKeyOnlyDocType(activeDocType);
    const nonDataMode = isActiveWorkNonDataDocType(activeDocType);
    const addHeaderBtn = $("btnAddHeaderSection");
    const bdmMixedAddWrap = $("bdmMixedAddWrap");
    if (addHeaderBtn) {
      if (isCensusMixed) {
        addHeaderBtn.textContent = "+ Add Census header section (before Coverpage)";
        addHeaderBtn.title =
          "Adds a new Census header section for this Mixed image, inserted before Coverpage.";
      } else {
        addHeaderBtn.textContent = "+ Add header section";
        addHeaderBtn.title = "";
      }
    }
    if (addHeaderWrap) {
      addHeaderWrap.classList.remove("header-add-section-wrap--inline");
    }
    const nonDataMessage = $("formNonDataMessage");
    if (nonDataMessage) nonDataMessage.hidden = !nonDataMode;
    if (headerBlock) headerBlock.hidden = nonDataMode;
    if (headerTitle) headerTitle.hidden = headerKeyOnlyMode;
    if (addHeaderWrap) addHeaderWrap.hidden = headerKeyOnlyMode || nonDataMode || isBdmMixed;
    if (bdmMixedAddWrap) bdmMixedAddWrap.hidden = headerKeyOnlyMode || !isBdmMixed;
    const headerList =
      Array.isArray(w.headers) && w.headers.length
        ? w.headers
        : w.header && w.header_id != null
          ? [{ header_id: w.header_id, section: null, fields: w.header }]
          : [];
    const bdmMixedHasCoverpageHeader = headerList.some(
      (hh) => hh && isCoverpageDocType(hh.form_type)
    );
    state.work.headers = headerList;
    if (headerList[0]) {
      state.work.header_id = headerList[0].header_id;
      state.work.header = headerList[0].fields;
    }
    const totalHeaders = headerList.length;
    if (headerKeyOnlyMode) {
      const coverFields =
        (headerList[0] && headerList[0].fields) ||
        (w.header && typeof w.header === "object" ? w.header : {});
      const coverHead = document.createElement("div");
      coverHead.className = "header-section__head";
      const coverTitle = document.createElement("h3");
      coverTitle.className = "header-section__title";
      coverTitle.textContent = String(activeDocType || "DocType");
      coverHead.appendChild(coverTitle);
      const coverOptions = { forHeader: false, allowHeaderFields: true };
      const coverBases = getVisibleFieldBases(coverFields, coverOptions);
      const coverRows = coverBases
        .filter((base) => !isHiddenField(base, coverFields[base] || {}, coverOptions))
        .map((base) => {
          const existingMeta = coverFields[base];
          const emptyMeta = {
            label: resolveFieldLabel(base, base),
            threshold: null,
            orig: "",
            hwr: "",
            conf: "",
            rect: "",
          };
          return renderFieldRow(base, existingMeta || emptyMeta, null, null);
        });
      const coverInner = document.createElement("div");
      coverInner.className = "fields fields--table";
      coverInner.appendChild(buildFieldTable(coverRows));
      hf.appendChild(coverHead);
      hf.appendChild(coverInner);
    } else {
      headerList.forEach((hh, sectionIdx) => {
      const isLastHeaderSection = sectionIdx === totalHeaders - 1;
      if (isCensusMixed && addHeaderWrap && !addHeaderWrap.hidden && isLastHeaderSection) {
        addHeaderWrap.classList.add("header-add-section-wrap--inline");
        hf.appendChild(addHeaderWrap);
      }
      const section = document.createElement("div");
      section.className = "header-section";
      section.dataset.headerId = String(hh.header_id);
      const headRow = document.createElement("div");
      headRow.className = "header-section__head";
      const h3 = document.createElement("h3");
      h3.className = "header-section__title";
      if (isCensusMixed) {
        const isLastHeader = sectionIdx === totalHeaders - 1;
        h3.textContent = isLastHeader
          ? "Header - Coverpage (section " + totalHeaders + ")"
          : "Header - Census (section " + (sectionIdx + 1) + ")";
      } else {
        h3.textContent = "Header - section " + (sectionIdx + 1);
      }
      if (hh.section != null && hh.section !== "") {
        h3.title = "Section (DB): " + String(hh.section);
      }
      const actions = document.createElement("div");
      actions.className = "header-section__actions";
      const upBtn = document.createElement("button");
      upBtn.type = "button";
      upBtn.className = "btn btn--small header-section-move-btn";
      upBtn.dataset.action = "header-move-up";
      upBtn.dataset.headerIndex = String(sectionIdx);
      upBtn.title = "Monter ce Header";
      upBtn.textContent = "\u2191";
      if (sectionIdx <= 0 || totalHeaders < 2) upBtn.disabled = true;
      const downBtn = document.createElement("button");
      downBtn.type = "button";
      downBtn.className = "btn btn--small header-section-move-btn";
      downBtn.dataset.action = "header-move-down";
      downBtn.dataset.headerIndex = String(sectionIdx);
      downBtn.title = "Descendre ce Header";
      downBtn.textContent = "\u2193";
      if (sectionIdx >= totalHeaders - 1 || totalHeaders < 2) downBtn.disabled = true;
      const delBtn = document.createElement("button");
      delBtn.type = "button";
      delBtn.className = "btn btn--small header-section-delete-btn";
      delBtn.dataset.action = "header-delete";
      delBtn.dataset.headerId = String(hh.header_id);
      delBtn.title = "Supprimer ce Header (si aucun record rattaché)";
      delBtn.textContent = "Supprimer";
      if (totalHeaders < 2) delBtn.disabled = true;
      actions.appendChild(upBtn);
      actions.appendChild(downBtn);
      actions.appendChild(delBtn);
      headRow.appendChild(h3);
      headRow.appendChild(actions);
      const inner = document.createElement("div");
      inner.className = "fields fields--table";
      const hdrFields = hh.fields || {};
      const headerBases = getVisibleFieldBases(hdrFields, { forHeader: true });
      const headerRows = headerBases
        .filter((base) => !isHiddenField(base, hdrFields[base] || {}, { forHeader: true }))
        .map((base) => {
          const existingMeta = hdrFields[base];
          const emptyMeta = {
            label: resolveFieldLabel(base, base),
            threshold: null,
            orig: "",
            hwr: "",
            conf: "",
            rect: "",
          };
          return renderFieldRow(base, existingMeta || emptyMeta, null, hh.header_id);
        });
      if (isBdmMixed) {
        const eventTypeRow = renderHeaderFormTypeRow(hh.form_type, hh.header_id);
        if (eventTypeRow) headerRows.unshift(eventTypeRow);
      }
      inner.appendChild(buildFieldTable(headerRows));
      section.appendChild(headRow);
      section.appendChild(inner);
      hf.appendChild(section);
      });
      if (!isCensusMixed && addHeaderWrap && headerBlock && addHeaderWrap.parentElement !== headerBlock) {
        headerBlock.appendChild(addHeaderWrap);
      }
    }

    const rb = $("recordsBlock");
    rb.innerHTML = "";
    rb.hidden = headerKeyOnlyMode || nonDataMode;

    resetAutosaveState();
    resetHistoryState();
    primeOrigInputs($("headerFields"));

    $("formEmpty").hidden = true;
    $("formContent").hidden = false;
    const dictBtn = $("btnApplyDictionaryCorrections");
    if (dictBtn) dictBtn.disabled = false;
    const pageFillBtn = $("btnAiPageFill");
    if (pageFillBtn) pageFillBtn.disabled = false;
    const pageStructureBtn = $("btnAiPageStructure");
    if (pageStructureBtn) pageStructureBtn.disabled = false;

    function finishRecordsRender() {
      if (runtime.activeWorkLoadToken !== loadToken) return;
      renderRecordsBlock();
      primeOrigInputs($("recordsBlock"));
      applyEmptyFilter();
      requestAnimationFrame(() => {
        requestAnimationFrame(applyFieldTableColumnWidths);
      });
      if (!(pageImgEl && w.image_file_url)) {
        requestAnimationFrame(() => {
          focusFirstHeaderOrigInput();
        });
      }
      if (runtime.activeWorkLoadToken === loadToken) {
        setStatus("Prêt");
      }
    }

    if (headerKeyOnlyMode || nonDataMode) {
      finishRecordsRender();
    } else if (typeof requestIdleCallback === "function") {
      requestIdleCallback(() => finishRecordsRender(), { timeout: 120 });
    } else {
      requestAnimationFrame(() => finishRecordsRender());
    }

    const mixedDocTypeChoice = await maybePromptMixedDocTypeOnLoad(w, imageId, state.batch);
    if (mixedDocTypeChoice) {
      await updateCurrentImageDocType(mixedDocTypeChoice, {
        requireConfirm: false,
        pushToHistory: true,
      });
      return;
    }

    prefetchNeighborWork(imageId);
    void applyImageViewerTab();
  } catch (e) {
    setStatus(e.message, true);
  }
}


export function showImageListContextMenu(clientX, clientY, imageId) {
  const root = ensureImageListContextMenu();
  imageListContextMenu.imageId = imageId;
  root.hidden = false;
  root.style.left = "0px";
  root.style.top = "0px";
  const w = root.offsetWidth;
  const h = root.offsetHeight;
  let x = clientX;
  let y = clientY;
  if (x + w > window.innerWidth - 8) x = window.innerWidth - w - 8;
  if (y + h > window.innerHeight - 8) y = window.innerHeight - h - 8;
  if (x < 8) x = 8;
  if (y < 8) y = 8;
  root.style.left = `${x}px`;
  root.style.top = `${y}px`;
}



export async function selectImageByIndex(idx) {
  if (idx < 0 || idx >= state.images.length) return;
  const prevIndex = state.imageIndex;
  const imageId = state.images[idx].image_id;
  const batchName = state.batch;

  // Paint the new image and clear previous form values before autosave flush / work fetch.
  state.imageIndex = idx;
  highlightList();
  paintImageForSwitch(batchName, imageId);
  clearFormPaneForImageSwitch();
  setStatus("Affichage de l'image…");

  if (!(await flushAutosaveBeforeNavigation("image-switch"))) {
    state.imageIndex = prevIndex;
    highlightList();
    if (state.work && state.work.image_id != null) {
      paintImageForSwitch(state.batch, state.work.image_id);
    }
    runtime.pendingDisplayImageId = state.work ? state.work.image_id : null;
    const prevImage = state.images[prevIndex];
    if (prevImage) await loadWork(prevImage.image_id);
    return;
  }
  rememberLastBatchImage(state.batch, imageId);
  await loadWork(imageId);
}

/** @param {Array<{ image_id?: string|number }>} images */
function resolveInitialImageIndex(images, batchName) {
  if (!Array.isArray(images) || !images.length) return -1;
  const lastImageId = getLastBatchImagesMap()[String(batchName || "").trim()];
  if (lastImageId) {
    const savedIdx = images.findIndex((img) => String(img.image_id) === String(lastImageId));
    if (savedIdx >= 0) return savedIdx;
  }
  return 0;
}


export async function onBatchChange() {
  const previousBatch = state.batch;
  if (!(await flushAutosaveBeforeNavigation("batch-switch"))) {
    if ($("batchSelect") && previousBatch != null) $("batchSelect").value = previousBatch;
    return;
  }
  state.batch = $("batchSelect").value;
  updateVisibleStatusBar();
  if (state.batch) {
    writeLs(LAST_BATCH_LS, state.batch);
    scheduleUserPreferencesSave({ last_batch: state.batch });
  } else {
    writeLs(LAST_BATCH_LS, null);
    scheduleUserPreferencesSave({ last_batch: "" });
  }
  if (!state.batch) return;
  setStatus("Chargement du lot...");
  resetAutosaveState();
  resetHistoryState();
  state.images = [];
  state.imageIndex = -1;
  renderImageList();
  runtime.nextImageWork = null;
  clearWorkPrefetchCache();
  clearWorkMemoryCache();

  try {
    if (!Array.isArray(state.subProjects) || !state.subProjects.length) {
      await loadSubProjects();
    }
    await loadProjectKinds();
    const canContinue = await maybePromptBatchSubProjectAssignment();
    if (!canContinue) {
      setStatus("Assignation d'un sous-projet requise pour ce lot (Images sans base).", true);
      return;
    }
    await loadDocTypesForBatch();
    const meta = await api(`/api/batch/${encodeURIComponent(state.batch)}/meta`);
    state.specFields = meta.fields;
    if (meta.spec_source === "schema") {
      setStatus("Champs d\u00e9duits du sch\u00e9ma (aucun .xlsx \u00e0 la racine)");
    }
    const { images } = await api(`/api/batch/${encodeURIComponent(state.batch)}/images`);
    state.images = images;
    renderImageList();
    if (images.length) {
      const initialIdx = resolveInitialImageIndex(images, state.batch);
      await selectImageByIndex(initialIdx);
      document.dispatchEvent(new CustomEvent("datafix:batch-loaded"));
    } else setStatus("Aucune image dans la base");
  } catch (e) {
    setStatus(e.message, true);
  }
}


export function stepImage(delta) {
  selectImageByIndex(state.imageIndex + delta);
}
