import { AUTO_SKIP_AT_FIELDS_LS, AUTO_SKIP_VALIDATED_ROWS_LS, DEFAULT_SHORTCUT_BINDINGS, FONT_SIZE_RANGE_MAX, FONT_SIZE_RANGE_MIN, LAST_BATCH_LS, SUGGEST_DEBOUNCE_MS } from '../config/constants.js';
import { $ } from '../core/dom.js';
import { fieldsByKindConfig } from '../config/fields-config.js';
import { api, flushAutosaveWithBeacon } from '../core/api.js';
import { writeLs } from '../core/storage.js';
import { normalizeUiText, normalizeUiTree } from '../core/text.js';
import { applyRootFromInput, applySubProjectAssignmentFromPanel, closeFolder, onBatchChange, openFolder, refreshCurrentWorkView, stepImage, updateCurrentImageDocType } from '../services/batch.js';
import { maybePromptMixedDocTypeOnLoad } from '../services/mixed-doctype-prompt.js';
import { buildFieldEntries, buildFieldOrderStorageKey, buildFieldSelectionKey, normalizeFieldOrderForEntries } from '../services/field-selections.js';
import { markFieldSelectionsDirty, persistFieldPreferences, saveCurrentSelectionsAsDefaults, saveMergeHwrOrigColumnPreference, saveShortcutsPanelStartupPreference, saveShowLotAssignmentPromptPreference, saveShowMergedHwrTextPreference, saveSuggestRankingModePreference, scheduleUserPreferencesSave, syncIndividualNumberAutoIncrementUi, syncMergeHwrOrigColumnUi, syncShowLotAssignmentPromptUi, syncShowMergedHwrTextUi } from '../services/preferences.js';
import { _getEffectiveFieldConfig, getActiveSubProject, getConfiguredFieldsForActiveDocType, getCurrentProjectKind, isBdmMixedDocTypeActive, isEventTypeFieldBase, isOrigInputPersistable, normalizeDocTypeKey, resolveFieldConfigRootKind } from '../services/project-context.js';
import { handleHeaderAddSection, handleHeaderDeleteClick, handleHeaderFormTypeChange, handleHeaderMoveClick, handleRecordAddClick, handleRecordAssignHeader, handleRecordMoveClick, handleRecordRemoveClick, handleRecordTransferToPrevImage } from '../services/records.js';
import { clearSuggestTimer, ensureSuggestRoot, fetchSuggestionsForInput, hideSuggestions, primeOrigInputs, renderSuggestions, syncSuggestionHighlightFromState } from '../services/suggestions.js';
import { autosaveState, historyState, state, subProfilePanelState, suggestState, runtime } from '../state/app-state.js';
import { commitHistoryActionForInput, queueAutosaveForInput, redoLastEdit, resetAutosaveState, resetHistoryState, undoLastEdit } from '../state/autosave.js';
import { applyEmptyFilter, applyFieldTableColumnWidths, renderRecordsBlock, rowAllEmptyFromInputs, syncRecordFieldsFromDom } from './form/field-table.js';
import { adjustFloatingInputFontSize, adjustImageZoom, applyImageTransform, applyRectAutoZoomMode, applyRectFlashMode, applySnippetToggleMode, commitOrigFieldImmediately, findNextNonSkippableOrigInput, isOrigInputSkippable, panImage, refreshActiveOrigSnippet, shouldForceUppercase, zoomTowardClientPoint } from './highlight.js';
import { applyImageViewerTab, imageViewUrl, renderActRectsOverlay, syncBdmImageTabs, toggleImageViewerTab, updateBdmToolbarVisibility } from './image-viewer.js';
import { closeImporterPanel, confirmDictionaryImport, openImporterPanel, parseDictionaryFiles } from './importer.js';
import { applyLayoutColumns, renderImageList } from './layout.js';
import { showAutocorrectChoicePanel, showConfirmActionPanel } from './modals.js';
import { loadGatewayActivationStatus } from './gateway-activation.js';
import { initPageStructureSuggestion } from './page-structure-suggestion.js';
import { renderSubProjectCiProfile } from './sub-project-ci-profile.js';
import { closeSettingsPanel, closeSubProjectAssignPanel, commitSubProject, fillSubProjectLanguageSelect, loadProjectKinds, loadSubProjects, openSettingsPanel, renderFieldsByKind, syncFieldsKindTabToCurrentProject } from './settings/panel.js';
import { closeShortcutsPanel, getEffectiveShortcutBinding, hideShortcutRebindPopover, openNonRebindableShortcutNotice, openShortcutRebindCapture, persistShortcutBindingOverrides, renderShortcutLists, showUiFloatingToast } from './shortcuts.js';
import { setStatus, updateVisibleStatusBar } from './status-bar.js';
import { applyFontScale, applyTheme, syncFontSizeSettingsUi, updateAppearanceFontPreviewHint } from './theme.js';

export function resetFieldSelectionsToDefaults() {
  state.fieldSelections = { ...state.fieldSelectionDefaults };
  state.fieldOrders = Object.fromEntries(
    Object.entries(state.fieldOrderDefaults || {}).map(([key, order]) => [
      key,
      Array.isArray(order) ? [...order] : [],
    ])
  );
  state.individualNumberAutoIncrement = !!state.individualNumberAutoIncrementDefault;
  syncIndividualNumberAutoIncrementUi();
  markFieldSelectionsDirty();
}


export function selectAllFieldsForKind(kind) {
  const config = _getEffectiveFieldConfig(kind);
  let changed = false;
  Object.entries(config).forEach(([docType, fields]) => {
    buildFieldEntries(fields).forEach((entry) => {
      const field = entry.field || {};
      const selectionKey = buildFieldSelectionKey(kind, docType, field.fieldName || "");
      if (!state.fieldSelections[selectionKey]) {
        state.fieldSelections[selectionKey] = true;
        changed = true;
      }
    });
  });
  if (changed) {
    markFieldSelectionsDirty();
  }
  return changed;
}


export function moveFieldOrderBefore(kind, docType, fields, fromId, toId) {
  if (!fromId || !toId || fromId === toId) return false;
  const entries = buildFieldEntries(fields);
  const orderKey = buildFieldOrderStorageKey(kind, docType);
  const order = normalizeFieldOrderForEntries(state.fieldOrders[orderKey], entries);
  const fromIndex = order.indexOf(fromId);
  const toIndex = order.indexOf(toId);
  if (fromIndex < 0 || toIndex < 0) return false;
  const next = [...order];
  const [moved] = next.splice(fromIndex, 1);
  next.splice(toIndex, 0, moved);
  state.fieldOrders[orderKey] = next;
  return true;
}


export const HIDDEN_FIELD_KEYS = new Set([
  "browselevel",
  "browselevel1",
  "publicationarchivelocation",
  "publicationarchivename",
]);


export function normalizeFieldKey(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]/g, "");
}


export function shouldKeepNonConfiguredFieldsVisible(options) {
  return true;
}


export function hasHwrOrOrigData(meta) {
  if (!meta) return false;
  const hwr = String(meta.hwr == null ? "" : meta.hwr).trim();
  const orig = String(meta.orig == null ? "" : meta.orig).trim();
  return hwr !== "" || orig !== "";
}


export function getConfiguredFieldNameSetForDocType(kind, docType) {
  const configured = getConfiguredFieldsForActiveDocType(kind, docType);
  return new Set(
    (configured || [])
      .map((f) => String((f && f.fieldName) || "").trim())
      .filter(Boolean)
  );
}


export function pruneFieldsObjectToAllowedSet(fieldsObj, allowedFieldNames, options) {
  const src = fieldsObj && typeof fieldsObj === "object" ? fieldsObj : {};
  const out = {};
  const keepFieldsWithData = !!(options && options.keepFieldsWithData);
  if (!allowedFieldNames || !allowedFieldNames.size) {
    if (!keepFieldsWithData) return out;
    Object.keys(src).forEach((base) => {
      if (hasHwrOrOrigData(src[base])) out[base] = src[base];
    });
    return out;
  }
  Object.keys(src).forEach((base) => {
    if (allowedFieldNames.has(base)) {
      out[base] = src[base];
      return;
    }
    if (keepFieldsWithData && hasHwrOrOrigData(src[base])) {
      out[base] = src[base];
    }
  });
  return out;
}


export const prefetchCache = {};

export const WORK_MEMORY_CACHE_MAX = 5;

const workMemoryCache = new Map();

export let currentPrefetchController = null;

const prefetchControllers = new Map();

export let imageViewerTabRequestToken = 0;

export function workPrefetchKey(batchName, imageId) {
  return `${String(batchName || "")}|${String(imageId)}`;
}

export function clearWorkPrefetchCache() {
  if (currentPrefetchController) {
    currentPrefetchController.abort();
    currentPrefetchController = null;
  }
  prefetchControllers.forEach((controller) => controller.abort());
  prefetchControllers.clear();
  for (let k in prefetchCache) {
    delete prefetchCache[k];
  }
}

export function clearWorkMemoryCache() {
  workMemoryCache.clear();
}

export function getCachedWorkPayload(batchName, imageId) {
  const key = workPrefetchKey(batchName, imageId);
  const entry = workMemoryCache.get(key);
  if (!entry) return null;
  entry.lastUsed = Date.now();
  return entry.payload;
}

export function setCachedWorkPayload(batchName, imageId, payload) {
  if (!batchName || imageId == null || !payload) return;
  const key = workPrefetchKey(batchName, imageId);
  workMemoryCache.set(key, {
    payload,
    lastUsed: Date.now(),
  });
  while (workMemoryCache.size > WORK_MEMORY_CACHE_MAX) {
    let oldestKey = null;
    let oldestTs = Infinity;
    workMemoryCache.forEach((entry, cacheKey) => {
      if (entry.lastUsed < oldestTs) {
        oldestTs = entry.lastUsed;
        oldestKey = cacheKey;
      }
    });
    if (oldestKey == null) break;
    workMemoryCache.delete(oldestKey);
  }
}

export function invalidateCachedWorkPayload(batchName, imageId) {
  if (!batchName) return;
  if (imageId == null) {
    const prefix = `${String(batchName)}|`;
    for (const key of Array.from(workMemoryCache.keys())) {
      if (key.startsWith(prefix)) workMemoryCache.delete(key);
    }
    return;
  }
  workMemoryCache.delete(workPrefetchKey(batchName, imageId));
}

export function consumePrefetchedWork(batchName, imageId) {
  const key = workPrefetchKey(batchName, imageId);
  const prefetched = prefetchCache[key];
  if (!prefetched) return null;
  delete prefetchCache[key];
  return prefetched;
}

export function bumpImageViewerTabRequestToken() {
  imageViewerTabRequestToken += 1;
  return imageViewerTabRequestToken;
}

export function getDesktopImageLabel() {
  const titleEl = $("imageTitle");
  const title = titleEl ? String(titleEl.textContent || "").trim() : "";
  if (title) return title;
  if (state.imageIndex >= 0 && state.imageIndex < state.images.length) {
    const current = state.images[state.imageIndex];
    if (current) {
      const name = current.image_name || "Image " + current.image_id;
      return name + " - ImageID " + current.image_id;
    }
  }
  return "";
}


export function getNextImageFor(imageId) {
  if (imageId == null) return null;
  const currIdx = state.images.findIndex((im) => im.image_id === imageId);
  if (currIdx === -1) return null;
  const nextIdx = currIdx + 1;
  if (nextIdx >= state.images.length) return null;
  return state.images[nextIdx];
}


export function setImageViewerTab(tabName) {
  const nextTab = String(tabName || "") === "next" ? "next" : "current";
  if (state.imageViewerTab === nextTab) {
    syncBdmImageTabs();
    return;
  }
  state.imageViewerTab = nextTab;
  syncBdmImageTabs();
  void applyImageViewerTab();
}


export function parseRecordIdMap(rawMap) {
  if (!rawMap || typeof rawMap !== "object") return null;
  const parsed = new Map();
  for (const [rawOld, rawNew] of Object.entries(rawMap)) {
    const oldId = parseInt(rawOld, 10);
    const newId = parseInt(rawNew, 10);
    if (Number.isFinite(oldId) && Number.isFinite(newId)) {
      parsed.set(oldId, newId);
    }
  }
  return parsed.size ? parsed : null;
}


export function remapEntityKeyWithRecordMap(entityKey, idMap) {
  if (!entityKey || !idMap || !entityKey.startsWith("record:")) return entityKey;
  const oldId = parseInt(entityKey.slice("record:".length), 10);
  if (!Number.isFinite(oldId)) return entityKey;
  const newId = idMap.get(oldId);
  return Number.isFinite(newId) ? `record:${newId}` : entityKey;
}


export function remapRecordSnapshotId(snapshot, idMap) {
  if (!snapshot || !idMap) return;
  const oldId = parseInt(snapshot.record_id, 10);
  if (!Number.isFinite(oldId)) return;
  const newId = idMap.get(oldId);
  if (Number.isFinite(newId)) snapshot.record_id = newId;
}


export function remapRecordIdsAfterServerRenumber(rawMap) {
  const idMap = parseRecordIdMap(rawMap);
  if (!idMap) return null;

  if (state.work && Array.isArray(state.work.records)) {
    state.work.records.forEach((rec) => {
      if (!rec) return;
      const oldId = parseInt(rec.record_id, 10);
      if (!Number.isFinite(oldId)) return;
      const newId = idMap.get(oldId);
      if (Number.isFinite(newId)) rec.record_id = newId;
    });
  }

  const remapHistoryAction = (action) => {
    if (!action || typeof action !== "object") return;
    if (action.kind === "field-edit") {
      action.entityKey = remapEntityKeyWithRecordMap(action.entityKey, idMap);
    } else if (action.kind === "record-move") {
      const oldMoveId = parseInt(action.recordId, 10);
      const newMoveId = Number.isFinite(oldMoveId) ? idMap.get(oldMoveId) : null;
      if (Number.isFinite(newMoveId)) action.recordId = newMoveId;
    } else if (action.kind === "record-insert" || action.kind === "record-remove") {
      remapRecordSnapshotId(action.snapshot, idMap);
    }
  };

  historyState.undoStack.forEach(remapHistoryAction);
  historyState.redoStack.forEach(remapHistoryAction);
  return idMap;
}


export function orderRecordsFromServerIds(rawIds) {
  if (!state.work || !Array.isArray(state.work.records) || !Array.isArray(rawIds)) return;
  const order = rawIds
    .map((id) => parseInt(id, 10))
    .filter((id) => Number.isFinite(id));
  if (!order.length) return;
  const byId = new Map();
  state.work.records.forEach((rec) => {
    if (!rec) return;
    const rid = parseInt(rec.record_id, 10);
    if (Number.isFinite(rid)) byId.set(rid, rec);
  });
  const ordered = [];
  order.forEach((rid) => {
    const rec = byId.get(rid);
    if (rec) {
      ordered.push(rec);
      byId.delete(rid);
    }
  });
  byId.forEach((rec) => ordered.push(rec));
  state.work.records = ordered;
}


export function beginHistoryEditSession(input) {
  if (!input) return;
  if (historyState.applying) return;
  if (historyState.pendingEdits.has(input)) return;
  historyState.pendingEdits.set(input, {
    startValue: input.value,
  });
}


export function mergePendingPayload(entityKey, payload) {
  const existing = autosaveState.pendingByEntity.get(entityKey);
  if (!existing) {
    autosaveState.pendingByEntity.set(entityKey, payload);
    return;
  }
  autosaveState.pendingByEntity.set(entityKey, {
    entity: existing.entity || payload.entity,
    updates: { ...existing.updates, ...payload.updates },
  });
}


export function positionSuggestionsForInput(input) {
  const root = ensureSuggestRoot();
  const rect = input.getBoundingClientRect();
  root.style.left = Math.round(rect.left) + "px";
  root.style.top = Math.round(rect.bottom + 2) + "px";
  root.style.width = Math.round(rect.width) + "px";
}


export function getSuggestionFieldNameFromInput(input) {
  const col = String((input && input.dataset && input.dataset.col) || "");
  const base = col.endsWith("_orig") ? col.slice(0, -5) : col;
  return String(base || "").trim() || null;
}


export function moveSuggestionHighlight(delta) {
  if (!suggestState.items.length) return;
  const size = suggestState.items.length;
  const current = suggestState.highlightedIndex < 0 ? -1 : suggestState.highlightedIndex;
  const next = ((current + delta) % size + size) % size;
  suggestState.highlightedIndex = next;
  syncSuggestionHighlightFromState();
}


export function scheduleSuggestionsForInput(input) {
  clearSuggestTimer();
  suggestState.activeInput = input;
  suggestState.timer = setTimeout(() => {
    suggestState.timer = null;
    void fetchSuggestionsForInput(input);
  }, SUGGEST_DEBOUNCE_MS);
}


export function isMacLikePlatform() {
  return (
    /Mac|iPhone|iPad|iPod/i.test(navigator.platform || "") ||
    (navigator.userAgentData && navigator.userAgentData.platform === "macOS")
  );
}


export function bindingLogicalKey(bind) {
  if (!bind) return null;
  const stored = String(bind.key || "").toLowerCase();
  if (stored.length === 1 && /^[a-z0-9]$/.test(stored)) return stored;
  const code = String(bind.code || "");
  if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
  if (/^Digit[0-9]$/.test(code)) return code.slice(5);
  return null;
}


export function bindingLogicalKeyFromEvent(e) {
  const key = String((e && e.key) || "").toLowerCase();
  if (key.length === 1 && /^[a-z0-9]$/.test(key)) return key;
  return null;
}


export function bindingCodeToDisplayLabel(code) {
  const c = String(code || "");
  if (c.startsWith("Key")) return c.slice(3);
  if (c.startsWith("Digit")) return c.slice(5);
  const map = {
    Space: "Space",
    Minus: "-",
    Equal: "=",
    Backquote: "`",
    Tab: "Tab",
    Escape: "Esc",
    Enter: "Enter",
    BracketLeft: "[",
    BracketRight: "]",
    Backslash: "\\",
    Semicolon: ";",
    Quote: "'",
    Comma: ",",
    Period: ".",
    Slash: "/",
  };
  if (map[c]) return map[c];
  if (/^F\d+$/.test(c)) return c;
  if (c.startsWith("Arrow")) return c.replace("Arrow", "");
  return c || "?";
}


export function formatBindingDisplay(bind) {
  if (!bind) return "";
  const mac = isMacLikePlatform();
  const parts = [];
  if (bind.meta) parts.push(mac ? "Cmd" : "Win");
  if (bind.ctrl) parts.push("Ctrl");
  if (bind.alt) parts.push("Alt");
  if (bind.shift) parts.push("Shift");
  const logical = bindingLogicalKey(bind);
  parts.push(logical || bindingCodeToDisplayLabel(bind.code));
  return parts.join("+");
}


export function shortcutsBindingsEqual(a, b) {
  if (!a || !b) return false;
  const modsMatch =
    !!a.ctrl === !!b.ctrl &&
    !!a.alt === !!b.alt &&
    !!a.shift === !!b.shift &&
    !!a.meta === !!b.meta;
  if (!modsMatch) return false;
  const la = bindingLogicalKey(a);
  const lb = bindingLogicalKey(b);
  if (la && lb) return la === lb;
  return a.code === b.code;
}


export function eventMatchesShortcutBinding(e, bindId) {
  const b = getEffectiveShortcutBinding(bindId);
  if (!b) return false;
  const modsMatch =
    !!e.ctrlKey === !!b.ctrl &&
    !!e.altKey === !!b.alt &&
    !!e.shiftKey === !!b.shift &&
    !!e.metaKey === !!b.meta;
  if (!modsMatch) return false;
  const logical = bindingLogicalKey(b);
  if (logical) return bindingLogicalKeyFromEvent(e) === logical;
  return e.code === b.code;
}


export function normalizeBindingFromEvent(e) {
  const binding = {
    ctrl: !!e.ctrlKey,
    alt: !!e.altKey,
    shift: !!e.shiftKey,
    meta: !!e.metaKey,
    code: e.code || "",
  };
  const logical = bindingLogicalKeyFromEvent(e);
  if (logical) binding.key = logical;
  return binding;
}


export const SHORTCUT_CAPTURE_MODIFIER_CODES = new Set([
  "ControlLeft",
  "ControlRight",
  "AltLeft",
  "AltRight",
  "ShiftLeft",
  "ShiftRight",
  "MetaLeft",
  "MetaRight",
]);


export function shortcutEventIsModifierOnly(e) {
  return (
    SHORTCUT_CAPTURE_MODIFIER_CODES.has(String(e.code || "")) ||
    ["Control", "Alt", "Shift", "Meta"].includes(String(e.key || ""))
  );
}


export function formatModifierPartsFromEvent(e) {
  const mac = isMacLikePlatform();
  const parts = [];
  if (e.metaKey) parts.push(mac ? "Cmd" : "Win");
  if (e.ctrlKey) parts.push("Ctrl");
  if (e.altKey) parts.push("Alt");
  if (e.shiftKey) parts.push("Shift");
  return parts;
}


export function clearShortcutRebindLivePreview() {
  const live = $("shortcutRebindLiveKeys");
  if (live) live.textContent = "";
}


export function validateCapturedShortcutBinding(b) {
  const code = String(b.code || "");
  if (!code) return "Touches non reconnues.";
  if (SHORTCUT_CAPTURE_MODIFIER_CODES.has(code)) return "Choisissez une touche en plus des modificateurs.";
  if (!b.ctrl && !b.alt && !b.meta)
    return "Utilisez au moins Ctrl, Alt ou Cmd (Win) avec la touche.";
  return "";
}


export function findShortcutBindingConflict(bindId, candidate) {
  const ids = Object.keys(DEFAULT_SHORTCUT_BINDINGS);
  for (let i = 0; i < ids.length; i++) {
    const id = ids[i];
    if (id === bindId) continue;
    const other = getEffectiveShortcutBinding(id);
    if (other && shortcutsBindingsEqual(candidate, other)) return id;
  }
  return null;
}


export function positionShortcutRebindPopoverNear(anchor) {
  const el = $("shortcutRebindPopover");
  if (!el || !anchor) return;
  const pad = 8;
  el.hidden = false;
  void el.offsetWidth;
  const r = anchor.getBoundingClientRect();
  let top = r.bottom + pad;
  let left = r.left;
  const pr = el.getBoundingClientRect();
  const maxLeft = window.innerWidth - pr.width - pad;
  left = Math.min(Math.max(left, pad), Math.max(pad, maxLeft));
  const maxTop = window.innerHeight - pr.height - pad;
  if (top + pr.height > window.innerHeight - pad) top = Math.max(pad, r.top - pr.height - pad);
  top = Math.min(Math.max(top, pad), Math.max(pad, maxTop));
  el.style.left = `${Math.round(left)}px`;
  el.style.top = `${Math.round(top)}px`;
}


export function endShortcutRebindSession() {
  if (runtime.shortcutRebindState.onKeyDoc) {
    window.removeEventListener("keydown", runtime.shortcutRebindState.onKeyDoc, true);
  }
  if (runtime.shortcutRebindState.onKeyUpDoc) {
    window.removeEventListener("keyup", runtime.shortcutRebindState.onKeyUpDoc, true);
  }
  if (runtime.shortcutRebindState.onMouseDoc) {
    document.removeEventListener("mousedown", runtime.shortcutRebindState.onMouseDoc, true);
  }
  runtime.shortcutRebindState = {
    active: false,
    mode: "info",
    bindId: null,
    onKeyDoc: null,
    onKeyUpDoc: null,
    onMouseDoc: null,
  };
  hideShortcutRebindPopover();
}


export function onShortcutsListActivateRow(shortcut, row) {
  if (runtime.shortcutRebindState.active) return;
  if (!shortcut.bindId) {
    openNonRebindableShortcutNotice(shortcut, row);
    return;
  }
  openShortcutRebindCapture(shortcut.bindId, row);
}


export function resetKeyboardShortcutsToDefaults() {
  endShortcutRebindSession();
  runtime.shortcutBindingsOverrides = {};
  persistShortcutBindingOverrides();
  renderShortcutLists();
}


export function isShortcutsPanelOpen() {
  const panel = $("shortcutsFloatingPanel");
  return !!(panel && !panel.hidden);
}


export let importerPendingFiles = [];

export let importerParseReady = false;

export function setImporterPendingFiles(files) {
  importerPendingFiles = Array.isArray(files) ? files : [];
}

export function setImporterParseReady(ready) {
  importerParseReady = !!ready;
}


export function isImporterPanelOpen() {
  const panel = $("importerPanel");
  return !!(panel && !panel.hidden);
}


export function setImporterRunVisible(visible) {
  const importBtn = $("btnImporterRun");
  const actions = importBtn ? importBtn.closest(".importer-panel__actions") : null;
  if (actions) {
    actions.hidden = !visible;
    return;
  }
  if (importBtn) importBtn.hidden = !visible;
}


export function resetImporterPreview() {
  const previewEl = $("importerPreview");
  const importBtn = $("btnImporterRun");
  importerParseReady = false;
  if (previewEl) {
    previewEl.hidden = true;
    previewEl.innerHTML = "";
  }
  setImporterRunVisible(true);
  if (importBtn) importBtn.disabled = true;
}


export function setImporterFeedback(message, isError) {
  const feedbackEl = $("importerFeedback");
  if (!feedbackEl) return;
  feedbackEl.textContent = normalizeUiText(String(message || ""));
  feedbackEl.classList.remove("importer-feedback--success", "importer-feedback--error");
  feedbackEl.classList.add(isError ? "importer-feedback--error" : "importer-feedback--success");
}


export function isDictionaryImportFile(file) {
  const name = String((file && file.name) || "").trim().toLowerCase();
  if (!name) return false;
  if (name.endsWith(".db3") && name.includes("dictionary")) return true;
  return name.endsWith(".txt") || name.endsWith(".json");
}


export function showDocTypeChangeOverlay() {
  runtime.docTypeChangeOverlayDepth += 1;
  const panel = $("docTypeChangeOverlay");
  const backdrop = $("docTypeChangeBackdrop");
  if (panel) panel.hidden = false;
  if (backdrop) backdrop.hidden = false;
}


export function hideDocTypeChangeOverlay() {
  runtime.docTypeChangeOverlayDepth = Math.max(0, runtime.docTypeChangeOverlayDepth - 1);
  if (runtime.docTypeChangeOverlayDepth > 0) return;
  const panel = $("docTypeChangeOverlay");
  const backdrop = $("docTypeChangeBackdrop");
  if (panel) panel.hidden = true;
  if (backdrop) backdrop.hidden = true;
}


export function showReorderOverlay() {
runtime.reorderOverlayDepth += 1;

const panel = $("reorderOverlay");

const backdrop = $("reorderOverlayBackdrop");
if (panel) panel.hidden = false;
if (backdrop) backdrop.hidden = false;
}


export function hideReorderOverlay() {
runtime.reorderOverlayDepth = Math.max(0, runtime.reorderOverlayDepth - 1);
if (runtime.reorderOverlayDepth > 0) return;

const panel = $("reorderOverlay");

const backdrop = $("reorderOverlayBackdrop");
if (panel) panel.hidden = true;
if (backdrop) backdrop.hidden = true;
}


export function resolveConfirmAction(result) {
  if (!runtime.confirmActionResolver) return;
  const resolver = runtime.confirmActionResolver;
  runtime.confirmActionResolver = null;
  const panel = $("confirmActionPanel");
  const backdrop = $("confirmActionBackdrop");
  if (panel) panel.hidden = true;
  if (backdrop) backdrop.hidden = true;
  resolver(result);
}

export function resolveAutocorrectChoice(result) {
  if (!runtime.autocorrectChoiceResolver) return;
  const resolver = runtime.autocorrectChoiceResolver;
  runtime.autocorrectChoiceResolver = null;
  const panel = $("autocorrectChoicePanel");
  const backdrop = $("autocorrectChoiceBackdrop");
  if (panel) panel.hidden = true;
  if (backdrop) backdrop.hidden = true;
  resolver(result);
}


export function resolveLotAssignment(result) {
  if (!runtime.lotAssignmentResolver) return;
  const resolver = runtime.lotAssignmentResolver;
  runtime.lotAssignmentResolver = null;
  const panel = $("lotAssignmentPanel");
  const backdrop = $("lotAssignmentBackdrop");
  if (panel) panel.hidden = true;
  if (backdrop) backdrop.hidden = true;
  resolver(result);
}


export function syncTypeToConfirmOkEnabled() {
  const input = $("typeToConfirmInput");
  const okBtn = $("btnTypeToConfirmOk");
  if (!(input instanceof HTMLInputElement) || !okBtn) return;
  const typed = String(input.value || "").trim();
  const expectedValues = Array.isArray(runtime.typeToConfirmExpectedValues)
    ? runtime.typeToConfirmExpectedValues
    : [];
  okBtn.disabled = !expectedValues.includes(typed);
}


export function resolveTypeToConfirm(result) {
  if (!runtime.typeToConfirmResolver) return;
  const resolver = runtime.typeToConfirmResolver;
  runtime.typeToConfirmResolver = null;
  runtime.typeToConfirmExpectedValues = [];
  runtime.subProjectDeleteConfirmResolver = null;
  const panel = $("typeToConfirmPanel");
  const backdrop = $("typeToConfirmBackdrop");
  const input = $("typeToConfirmInput");
  const okBtn = $("btnTypeToConfirmOk");
  if (panel) panel.hidden = true;
  if (backdrop) backdrop.hidden = true;
  if (input instanceof HTMLInputElement) input.value = "";
  if (okBtn) okBtn.disabled = true;
  resolver(!!result);
}


export function getCurrentBatchProjectInfo() {
  const batchName = String(state.batch || "").trim();
  if (!batchName) return null;
  return (state.projectKinds || []).find((p) => String(p.project_name || "") === batchName) || null;
}


export function getAssignableSubProjects(rootKind) {
  const rk = String(rootKind || "").trim().toLowerCase();
  return (state.subProjects || [])
    .filter((sp) => String(sp.root_kind || "").trim().toLowerCase() === rk)
    .map((sp) => String(sp.name || "").trim())
    .filter(Boolean);
}


export function resetWorkspaceAfterClose() {
  state.batch = "";
  state.images = [];
  state.imageIndex = -1;
  state.work = null;
  state.specFields = null;
  state.batchDocTypeGroups = null;
  state.docType = "";
  runtime.nextImageWork = null;
  writeLs(LAST_BATCH_LS, null);
  scheduleUserPreferencesSave({ last_batch: "" });
  const sel = $("batchSelect");
  if (sel) {
    sel.innerHTML = "";
    sel.value = "";
  }
  const projectRootInput = $("projectRoot");
  if (projectRootInput) projectRootInput.value = "";
  resetAutosaveState();
  resetHistoryState();
  renderImageList();
  const imgPanEl = $("imgPan");
  if (imgPanEl) imgPanEl.hidden = true;
  const pageImgEl = $("pageImg");
  if (pageImgEl) {
    pageImgEl.removeAttribute("src");
    pageImgEl.style.width = "";
    pageImgEl.style.height = "";
    pageImgEl.style.maxWidth = "";
    pageImgEl.style.maxHeight = "";
  }
  renderActRectsOverlay();
  const noImgEl = $("noImg");
  if (noImgEl) {
    noImgEl.textContent = "Ouvrez un dossier pour commencer.";
    noImgEl.hidden = false;
  }
  const imageTitleEl = $("imageTitle");
  if (imageTitleEl) {
    imageTitleEl.textContent = "";
    imageTitleEl.hidden = true;
  }
  $("formEmpty").hidden = false;
  $("formContent").hidden = true;
  const dictBtn = $("btnApplyDictionaryCorrections");
  if (dictBtn) dictBtn.disabled = true;
  const pageFillBtn = $("btnAiPageFill");
  if (pageFillBtn) pageFillBtn.disabled = true;
  updateVisibleStatusBar();
  updateBdmToolbarVisibility();
}


export function setActiveSettingsTab(target) {
  const tabs = Array.from(document.querySelectorAll(".settings-tab"));
  const pages = Array.from(document.querySelectorAll(".settings-page"));
  tabs.forEach((t) => {
    const active = t.dataset.settingsTab === target;
    t.classList.toggle("is-active", active);
    t.setAttribute("aria-selected", active ? "true" : "false");
  });
  pages.forEach((page) => {
    page.classList.toggle("is-active", page.dataset.settingsPage === target);
  });
  if (target === "fields") {
    syncFieldsKindTabToCurrentProject();
  }
  if (target === "projectKinds") {
    void Promise.all([loadProjectKinds(), loadSubProjects()]);
  }
  if (target === "ai") {
    loadGatewayActivationStatus();
  }
  if (target === "appearance") {
    syncFontSizeSettingsUi();
  }
}


/**
 * Return the effective field config for the Champs tab for a given name.
 * - If name matches a sub-project with CI fields Ã¢â€ â€™ build config from CI fields.
 * - If name matches a hardcoded fieldsByKindConfig key Ã¢â€ â€™ use that.
 * - If name matches a sub-project with no CI fields Ã¢â€ â€™ use fieldsByKindConfig for its root_kind.
 */

export function _buildCiFieldConfig(sp) {
  const toEntry = (f) => {
    if (!f || !f.field_name) return null;
    return {
      fieldLabel: String(f.label || f.field_name),
      fieldName: String(f.field_name),
      defaultSelected: true,
    };
  };
  const toEntries = (arr) =>
    Array.isArray(arr) ? arr.map(toEntry).filter(Boolean) : [];

  const config = {};

  // Header fields (from flat CI field list)
  const headerFields = toEntries((sp.fields || []).filter((f) => f.header_field));
  if (headerFields.length) config.__HEADER__ = headerFields;

  const dtFields = sp.doctype_fields && typeof sp.doctype_fields === "object"
    ? sp.doctype_fields
    : {};

  const dtSheetKeys = Object.keys(dtFields).filter((k) => !k.startsWith("__"));
  if (dtSheetKeys.length) {
    // Ã¢â€â‚¬Ã¢â€â‚¬ CI provides per-DocType field lists (from DocType sheets) Ã¢â€â‚¬Ã¢â€â‚¬
    dtSheetKeys.forEach((dt) => {
      const fields = dtFields[dt];
      if (dt === "Mixed") {
        // Mixed: expand each EventType as its own Champs column
        if (typeof fields === "object" && !Array.isArray(fields)) {
          Object.entries(fields).forEach(([et, etFields]) => {
            const mapped = toEntries(etFields);
            if (mapped.length) config[`Mixed \u2192 ${et}`] = mapped;
          });
        }
      } else if (Array.isArray(fields)) {
        const mapped = toEntries(fields);
        if (mapped.length) config[dt] = mapped;
      }
    });
  } else if (Array.isArray(sp.fields) && sp.fields.length) {
    // Ã¢â€â‚¬Ã¢â€â‚¬ Fall back: flat field list, duplicate across Data DocTypes Ã¢â€â‚¬Ã¢â€â‚¬
    const recordFields = toEntries(sp.fields.filter((f) => !f.header_field));
    const batchMatch = (state.projectKinds || []).find(
      (p) => p.project_name === state.batch && p.sub_project === sp.name
    );
    let dataDocTypes = [];
    if (batchMatch && state.batchDocTypeGroups) {
      dataDocTypes = state.batchDocTypeGroups.data || [];
    }
    if (!dataDocTypes.length) {
      const hdKeys = new Set(
        Object.keys(fieldsByKindConfig[sp.root_kind] || {}).filter((k) => !k.startsWith("__"))
      );
      dataDocTypes = (sp.doc_types || []).filter((dt) => hdKeys.has(dt));
    }
    if (!dataDocTypes.length) {
      dataDocTypes = sp.doc_types || [];
    }
    dataDocTypes.forEach((dt) => { config[dt] = recordFields; });
  }

  return config;
}

/** Generate kind-tab buttons from sub-projects (or fall back to Census/BDM). */

export function normalizeFieldsKindTab(kind) {
  const k = String(kind || "");
  // If it's a known sub-project name, return as-is
  if ((state.subProjects || []).some((sp) => sp.name === k)) return k;
  // Fall back to Census/BDM
  return k.toLowerCase() === "census" ? "Census" : "BDM";
}


export function setActiveFieldsKindTab(kind) {
  const k = normalizeFieldsKindTab(kind);
  state.selectedFieldsKind = k;
  const tabs = Array.from(document.querySelectorAll(".fields-kind-tab"));
  tabs.forEach((t) => {
    const active = t.dataset.kindTab === k;
    t.classList.toggle("is-active", active);
    t.setAttribute("aria-selected", active ? "true" : "false");
  });
  renderFieldsByKind(k);
}


export let _editorDocTypes = [];

export let _editorFields = /** @type {Array<object>} */ ([]);

export let _editorDoctypeFields = /** @type {Object} */ ({});

export let _editorKeyingInstructions = /** @type {Array<object>} */ ([]);


export function _setEditorDocTypes(list) {
  _editorDocTypes = list;
  // Sync hidden input with plain names (for commitSubProject to read)
  const hidden = $("subProjectEditorDocTypes");
  if (hidden) hidden.value = list.map((d) => d.name).join(",");
  _renderDocTypeTags();
}

export function _setEditorFields(list) {
  _editorFields = Array.isArray(list) ? list : [];
}

export function _setEditorDoctypeFields(obj) {
  _editorDoctypeFields =
    obj && typeof obj === "object" ? obj : {};
}

export function _setEditorKeyingInstructions(list) {
  _editorKeyingInstructions = Array.isArray(list) ? list : [];
}


export function _renderDocTypeTags() {
  const wrap = $("subProjectDocTypesPreview");
  if (!wrap) return;
  wrap.innerHTML = "";
  if (!_editorDocTypes.length) {
    const msg = document.createElement("span");
    msg.className = "muted";
    msg.style.fontSize = "0.78rem";
    msg.textContent = "Aucun DocType \u2014 importez un fichier CI.";
    wrap.appendChild(msg);
    return;
  }
  _editorDocTypes.forEach((dt, idx) => {
    const tag = document.createElement("span");
    const catClass =
      dt.cat === "data"
        ? "doctype-tag--data"
        : dt.cat === "non_data"
        ? "doctype-tag--non-data"
        : "";
    tag.className = "doctype-tag " + catClass;

    const label = document.createElement("span");
    label.textContent = dt.name;
    tag.appendChild(label);

    if (dt.cat) {
      const catSpan = document.createElement("span");
      catSpan.className = "doctype-tag__cat";
      catSpan.textContent = dt.cat === "data" ? "Data" : "Non-Data";
      tag.appendChild(catSpan);
    }

    if (dt.cat === "data") {
      const headerOnlyLabel = document.createElement("label");
      headerOnlyLabel.className = "doctype-tag__header-only-toggle";
      headerOnlyLabel.title = "Ce DocType Data contient uniquement les champs Header.";
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.checked = !!dt.headerKeyOnly;
      checkbox.setAttribute("aria-label", "Header Key Only pour " + dt.name);
      checkbox.addEventListener("change", () => {
        _editorDocTypes[idx] = Object.assign({}, _editorDocTypes[idx], {
          headerKeyOnly: checkbox.checked,
        });
      });
      const headerOnlyText = document.createElement("span");
      headerOnlyText.textContent = "Header Key Only";
      headerOnlyLabel.appendChild(checkbox);
      headerOnlyLabel.appendChild(headerOnlyText);
      tag.appendChild(headerOnlyLabel);
    }

    const removeBtn = document.createElement("button");
    removeBtn.type = "button";
    removeBtn.className = "doctype-tag__remove";
    removeBtn.setAttribute("aria-label", "Supprimer " + dt.name);
    removeBtn.textContent = "\u00d7";
    removeBtn.addEventListener("click", () => {
      _editorDocTypes = _editorDocTypes.filter((_, i) => i !== idx);
      _setEditorDocTypes(_editorDocTypes);
    });
    tag.appendChild(removeBtn);

    wrap.appendChild(tag);
  });
}


export function _setCiImportStatus(msg, isError) {
  const el = $("ciImportStatus");
  if (!el) return;
  el.textContent = msg;
  el.className = "ci-import-status" + (isError ? " is-error" : "");
}


export function _buildDefaultFieldsFromConfig(subProjectName) {
  const config = fieldsByKindConfig[subProjectName];
  if (!config) return [];

  const seen = new Set();
  const out = [];

  const push = (f, isHeader) => {
    if (!f || !f.fieldName || seen.has(f.fieldName)) return;
    seen.add(f.fieldName);
    out.push({
      label: String(f.fieldLabel || f.fieldName),
      field_name: String(f.fieldName),
      header_field: !!isHeader,
    });
  };

  // Header fields first
  (config.__HEADER__ || []).forEach((f) => push(f, true));

  // All DocType groups (including __GENERAL__ for BDM)
  Object.entries(config).forEach(([key, val]) => {
    if (key === "__HEADER__") return;
    if (Array.isArray(val)) val.forEach((f) => push(f, false));
  });

  return out;
}

/** Prefer embedded CI groups on the sub-project; else batch DocType groups from the API. */

export function _docTypeGroupsSourceForProfile(sp) {
  const dtf = sp.doctype_fields && typeof sp.doctype_fields === "object" ? sp.doctype_fields : null;
  if (dtf && dtf.__doc_type_groups__ && typeof dtf.__doc_type_groups__ === "object") {
    return Object.assign({}, dtf.__doc_type_groups__, {
      header_key_only: Array.isArray(sp.header_key_only) ? sp.header_key_only : [],
    });
  }
  if (state.batchDocTypeGroups && typeof state.batchDocTypeGroups === "object") {
    return state.batchDocTypeGroups;
  }
  return null;
}


export function _bucketDocTypesForProfileList(docTypesRaw, groups) {
  const norm = (s) => String(s || "").trim().toLowerCase();
  const dataS = new Set((groups.data || []).map(norm));
  const nonS = new Set((groups.non_data || []).map(norm));
  const headerOnlyS = new Set((groups.header_key_only || []).map(norm));
  const data = [];
  const nonData = [];
  const headerKeyOnly = [];
  const seenN = new Set();
  (docTypesRaw || []).forEach((raw) => {
    const name = String(raw || "").trim();
    if (!name) return;
    const n = norm(name);
    if (seenN.has(n)) return;
    seenN.add(n);
    if (dataS.has(n)) {
      data.push(name);
      if (headerOnlyS.has(n)) headerKeyOnly.push(name);
    }
    else if (nonS.has(n)) nonData.push(name);
    else nonData.push(name);
  });
  const cmp = (a, b) => a.localeCompare(b, undefined, { sensitivity: "base" });
  data.sort(cmp);
  nonData.sort(cmp);
  headerKeyOnly.sort(cmp);
  return { data, nonData, headerKeyOnly };
}


function _normProfileDocTypeName(raw) {
  return String(raw || "").trim();
}


function _normProfileDocTypeKey(raw) {
  return _normProfileDocTypeName(raw).toLowerCase();
}


/** Flat list of DocTypes with Data / Non-Data category for the profile panel. */
export function _getProfileDocTypeEntries(subProject) {
  const dts = Array.isArray(subProject.doc_types) ? subProject.doc_types : [];
  const gSrc = _docTypeGroupsSourceForProfile(subProject);
  if (gSrc) {
    const buckets = _bucketDocTypesForProfileList(dts, gSrc);
    const headerOnlyKeys = new Set(buckets.headerKeyOnly.map(_normProfileDocTypeKey));
    const out = [];
    buckets.data.forEach((name) => out.push({
      name,
      cat: "data",
      headerKeyOnly: headerOnlyKeys.has(_normProfileDocTypeKey(name)),
    }));
    buckets.nonData.forEach((name) => out.push({ name, cat: "non_data", headerKeyOnly: false }));
    return out;
  }
  return dts
    .map((raw) => _normProfileDocTypeName(raw))
    .filter(Boolean)
    .map((name) => ({ name, cat: "non_data", headerKeyOnly: false }));
}


export function _applyProfileDocTypeEntries(subProject, entries) {
  const data = [];
  const nonData = [];
  const docTypes = [];
  const headerKeyOnly = [];
  const seen = new Set();
  (entries || []).forEach((entry) => {
    const name = _normProfileDocTypeName(entry && entry.name);
    if (!name) return;
    const key = _normProfileDocTypeKey(name);
    if (seen.has(key)) return;
    seen.add(key);
    docTypes.push(name);
    if (entry.cat === "data") {
      data.push(name);
      if (entry.headerKeyOnly) headerKeyOnly.push(name);
    }
    else nonData.push(name);
  });
  subProject.doc_types = docTypes;
  subProject.header_key_only = headerKeyOnly;
  const existing =
    subProject.doctype_fields && typeof subProject.doctype_fields === "object"
      ? subProject.doctype_fields
      : {};
  subProject.doctype_fields = Object.assign({}, existing, {
    __doc_type_groups__: { data, non_data: nonData },
  });
}


/** @returns {"empty" | "duplicate" | "added" | "moved"} */
export function _addProfileDocType(subProject, rawName, cat) {
  const name = _normProfileDocTypeName(rawName);
  if (!name) return "empty";
  const normalizedCat = cat === "data" ? "data" : "non_data";
  const entries = _getProfileDocTypeEntries(subProject);
  const key = _normProfileDocTypeKey(name);
  const existingIdx = entries.findIndex((e) => _normProfileDocTypeKey(e.name) === key);
  if (existingIdx >= 0) {
    const existing = entries[existingIdx];
    if (existing.cat === normalizedCat) return "duplicate";
    entries[existingIdx] = { name, cat: normalizedCat, headerKeyOnly: false };
    _applyProfileDocTypeEntries(subProject, entries);
    return "moved";
  }
  entries.push({ name, cat: normalizedCat, headerKeyOnly: false });
  _applyProfileDocTypeEntries(subProject, entries);
  return "added";
}


export function _removeProfileDocType(subProject, rawName, cat) {
  const key = _normProfileDocTypeKey(rawName);
  if (!key) return;
  const normalizedCat = cat === "data" ? "data" : "non_data";
  const entries = _getProfileDocTypeEntries(subProject).filter(
    (e) => !(_normProfileDocTypeKey(e.name) === key && e.cat === normalizedCat)
  );
  _applyProfileDocTypeEntries(subProject, entries);
}


function _appendEditableProfileDocTypeTag(inner, name, cat, subProject, headerKeyOnly) {
  const tag = document.createElement("span");
  tag.className = "doctype-tag";
  const label = document.createElement("span");
  label.textContent = String(name);
  tag.appendChild(label);
  if (cat === "data") {
    const headerOnlyLabel = document.createElement("label");
    headerOnlyLabel.className = "doctype-tag__header-only-toggle";
    headerOnlyLabel.title = "Ce DocType Data contient uniquement les champs Header.";
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = !!headerKeyOnly;
    checkbox.setAttribute("aria-label", "Header Key Only pour " + name);
    checkbox.addEventListener("change", (e) => {
      e.stopPropagation();
      const entries = _getProfileDocTypeEntries(subProject);
      const key = _normProfileDocTypeKey(name);
      const entry = entries.find(
        (candidate) => candidate.cat === "data" && _normProfileDocTypeKey(candidate.name) === key
      );
      if (!entry) return;
      entry.headerKeyOnly = checkbox.checked;
      _applyProfileDocTypeEntries(subProject, entries);
    });
    const headerOnlyText = document.createElement("span");
    headerOnlyText.textContent = "Header Key Only";
    headerOnlyLabel.appendChild(checkbox);
    headerOnlyLabel.appendChild(headerOnlyText);
    tag.appendChild(headerOnlyLabel);
  }
  const removeBtn = document.createElement("button");
  removeBtn.type = "button";
  removeBtn.className = "doctype-tag__remove";
  removeBtn.setAttribute("aria-label", "Supprimer " + name);
  removeBtn.textContent = "\u00d7";
  removeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    _removeProfileDocType(subProject, name, cat);
    _renderProfilePanel(subProject);
  });
  tag.appendChild(removeBtn);
  inner.appendChild(tag);
}


function _appendProfileDocTypeAddRow(inner, cat, subProject) {
  const row = document.createElement("div");
  row.className = "sub-profile-doctype-add-row";
  row.dataset.doctypeAddCat = cat;
  const inp = document.createElement("input");
  inp.type = "text";
  inp.className = "sub-profile-doctype-add-input";
  inp.placeholder = "Nouveau DocType\u2026";
  inp.autocomplete = "off";
  inp.setAttribute(
    "aria-label",
    cat === "data" ? "Ajouter un DocType Data" : "Ajouter un DocType Non-Data"
  );
  const btn = document.createElement("button");
  btn.type = "button";
  btn.className = "btn btn--small sub-profile-doctype-add-btn";
  btn.textContent = "Ajouter";
  const commit = () => {
    const result = _addProfileDocType(subProject, inp.value, cat);
    if (result === "empty") return;
    if (result === "duplicate") {
      setStatus("Ce DocType existe d\u00e9j\u00e0 dans cette cat\u00e9gorie.", true);
      return;
    }
    subProfilePanelState.pendingDocTypeAddFocus = cat;
    _renderProfilePanel(subProject);
  };
  btn.addEventListener("click", commit);
  inp.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      commit();
    }
  });
  row.appendChild(inp);
  row.appendChild(btn);
  inner.appendChild(row);
}


function _renderEditableProfileDocTypes(dtWrap, subProject) {
  dtWrap.innerHTML = "";
  dtWrap.classList.remove("doctype-tags-wrap");
  const entries = _getProfileDocTypeEntries(subProject);
  const cmp = (a, b) => a.localeCompare(b, undefined, { sensitivity: "base" });
  const dataNames = entries.filter((e) => e.cat === "data").map((e) => e.name).sort(cmp);
  const nonDataNames = entries
    .filter((e) => e.cat !== "data")
    .map((e) => e.name)
    .sort(cmp);
  const addGroup = (title, names, titleVariant, cat) => {
    const sect = document.createElement("div");
    sect.className =
      "sub-profile-doctype-group" +
      (titleVariant ? " sub-profile-doctype-group--" + titleVariant : "");
    const tl = document.createElement("div");
    tl.className =
      "sub-profile-doctype-group__title" +
      (titleVariant ? " sub-profile-doctype-group__title--" + titleVariant : "");
    tl.textContent = title;
    sect.appendChild(tl);
    const inner = document.createElement("div");
    inner.className = "doctype-tags-wrap";
    names.forEach((name) => {
      const entry = entries.find(
        (candidate) => candidate.cat === cat
          && _normProfileDocTypeKey(candidate.name) === _normProfileDocTypeKey(name)
      );
      _appendEditableProfileDocTypeTag(
        inner,
        name,
        cat,
        subProject,
        !!(entry && entry.headerKeyOnly)
      );
    });
    _appendProfileDocTypeAddRow(inner, cat, subProject);
    sect.appendChild(inner);
    dtWrap.appendChild(sect);
  };
  addGroup("Data", dataNames, "data", "data");
  addGroup("Non-Data", nonDataNames, "non-data", "non_data");
  normalizeUiTree(dtWrap);
  const focusCat = subProfilePanelState.pendingDocTypeAddFocus;
  if (focusCat) {
    subProfilePanelState.pendingDocTypeAddFocus = null;
    const row = dtWrap.querySelector(
      `.sub-profile-doctype-add-row[data-doctype-add-cat="${focusCat}"]`
    );
    const focusInp = row && row.querySelector(".sub-profile-doctype-add-input");
    if (focusInp instanceof HTMLInputElement) focusInp.focus();
  }
}


export function _profilePanelUpdateEditChrome() {
  const mode = subProfilePanelState.thresholdEditMode;
  const searchWrap = $("subProfileFooterSearchWrap");
  if (searchWrap) searchWrap.hidden = !mode;
  const saveBtn = $("btnProfileThresholdSave");
  if (saveBtn) saveBtn.hidden = !mode;
  const editBtn = $("btnProfileEdit");
  if (editBtn) {
    editBtn.textContent = mode ? "Annuler" : "Modifier";
    editBtn.title = mode
      ? "Annuler les modifications"
      : "Modifier le nom, la langue, les DocTypes et les seuils Rekey";
  }
  const delBtn = $("btnProfileDelete");
  if (delBtn instanceof HTMLButtonElement) {
    delBtn.hidden = mode;
  }
}


export function _applySubProfileFieldSearchFilter() {
  const q = (subProfilePanelState.fieldSearchQuery || "").trim().toLowerCase();
  document.querySelectorAll("#subProfileFieldsBody tr[data-profile-search-hay]").forEach((tr) => {
    if (!(tr instanceof HTMLElement)) return;
    const hay = (tr.dataset.profileSearchHay || "").toLowerCase();
    tr.hidden = q.length > 0 && !hay.includes(q);
  });
}


export function cancelProfilePanelThresholdEdit() {
  if (!subProfilePanelState.thresholdEditMode) return;
  const snap = subProfilePanelState.editSnapshot;
  const sp = subProfilePanelState.subProject;
  if (snap && sp) {
    sp.name = snap.name;
    sp.language = snap.language;
    sp.integral_keying = snap.integral_keying;
    sp.fields = snap.fields;
    sp.keying_instructions = snap.keying_instructions;
    sp.doc_types = snap.doc_types;
    sp.header_key_only = snap.header_key_only;
    sp.doctype_fields = snap.doctype_fields;
  }
  subProfilePanelState.editSnapshot = null;
  subProfilePanelState.pendingDocTypeAddFocus = null;
  subProfilePanelState.thresholdEditMode = false;
  subProfilePanelState.fieldSearchQuery = "";
  const sInp = $("subProfileFieldSearch");
  if (sInp instanceof HTMLInputElement) sInp.value = "";
  _profilePanelUpdateEditChrome();
  _renderProfilePanel(subProfilePanelState.subProject);
}


export function _renderProfilePanel(subProject) {
  if (!subProject) return;

  // Title + kind badge
  const title = $("subProfileTitle");
  const titleInput = $("subProfileTitleInput");
  const languageInput = $("subProfileLanguageInput");
  const languageValue = $("subProfileLanguageValue");
  if (subProfilePanelState.thresholdEditMode) {
    if (title) title.hidden = true;
    if (titleInput instanceof HTMLInputElement) {
      titleInput.hidden = false;
      if (document.activeElement !== titleInput) {
        titleInput.value = String(subProject.name || "");
      }
    }
    if (languageInput instanceof HTMLSelectElement) {
      languageInput.hidden = false;
      if (document.activeElement !== languageInput) {
        fillSubProjectLanguageSelect(languageInput, subProject.language);
      }
    }
    if (languageValue) languageValue.hidden = true;
    const integralEdit = $("subProfileIntegralKeyingEdit");
    const integralValue = $("subProfileIntegralKeyingValue");
    const integralInput = $("subProfileIntegralKeyingInput");
    if (integralEdit) integralEdit.hidden = false;
    if (integralValue) integralValue.hidden = true;
    if (integralInput instanceof HTMLInputElement) {
      integralInput.checked = Boolean(subProject.integral_keying);
    }
  } else {
    if (title) {
      title.hidden = false;
      title.textContent = String(subProject.name || "");
    }
    if (titleInput instanceof HTMLInputElement) titleInput.hidden = true;
    if (languageInput instanceof HTMLSelectElement) languageInput.hidden = true;
    if (languageValue) {
      const language = String(subProject.language || "").trim();
      languageValue.hidden = false;
      languageValue.textContent = language || "—";
    }
    const integralEdit = $("subProfileIntegralKeyingEdit");
    const integralValue = $("subProfileIntegralKeyingValue");
    if (integralEdit) integralEdit.hidden = true;
    if (integralValue) {
      integralValue.hidden = false;
      integralValue.textContent = subProject.integral_keying ? "Oui" : "Non";
    }
  }
  const kindBadge = $("subProfileKindBadge");
  if (kindBadge) {
    const isCensus = String(subProject.root_kind || "").toLowerCase() === "census";
    kindBadge.className = "kind-badge " + (isCensus ? "kind-badge--census" : "kind-badge--bdm");
    kindBadge.textContent = isCensus ? "Census" : "BDM";
  }
  // DocTypes (optionally grouped Data / Non-Data)
  const dtWrap = $("subProfileDocTypes");
  if (dtWrap) {
    if (subProfilePanelState.thresholdEditMode) {
      _renderEditableProfileDocTypes(dtWrap, subProject);
    } else {
      dtWrap.classList.add("doctype-tags-wrap");
      dtWrap.innerHTML = "";
      const dts = Array.isArray(subProject.doc_types) ? subProject.doc_types : [];
      if (!dts.length) {
        const msg = document.createElement("span");
        msg.className = "muted";
        msg.style.fontSize = "0.8rem";
        msg.textContent = "Aucun DocType d\u00e9fini.";
        dtWrap.appendChild(msg);
      } else {
        const gSrc = _docTypeGroupsSourceForProfile(subProject);
        const appendFlatSorted = () => {
          dts
            .slice()
            .map((x) => String(x || "").trim())
            .filter(Boolean)
            .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
            .forEach((name) => {
              const tag = document.createElement("span");
              tag.className = "doctype-tag";
              tag.textContent = name;
              dtWrap.appendChild(tag);
            });
        };
        if (gSrc) {
          const buckets = _bucketDocTypesForProfileList(dts, gSrc);
          const headerOnlyKeys = new Set(buckets.headerKeyOnly.map(_normProfileDocTypeKey));
          const addGroup = (title, names, titleVariant) => {
            if (!names.length) return;
            const sect = document.createElement("div");
            sect.className =
              "sub-profile-doctype-group" +
              (titleVariant ? " sub-profile-doctype-group--" + titleVariant : "");
            const tl = document.createElement("div");
            tl.className =
              "sub-profile-doctype-group__title" +
              (titleVariant ? " sub-profile-doctype-group__title--" + titleVariant : "");
            tl.textContent = title;
            sect.appendChild(tl);
            const inner = document.createElement("div");
            inner.className = "doctype-tags-wrap";
            names.forEach((name) => {
              const tag = document.createElement("span");
              tag.className = "doctype-tag";
              const label = document.createElement("span");
              label.textContent = String(name);
              tag.appendChild(label);
              if (headerOnlyKeys.has(_normProfileDocTypeKey(name))) {
                const badge = document.createElement("span");
                badge.className = "doctype-tag__header-only-badge";
                badge.textContent = "Header Key Only";
                tag.appendChild(badge);
              }
              inner.appendChild(tag);
            });
            sect.appendChild(inner);
            dtWrap.appendChild(sect);
          };
          addGroup("Data", buckets.data, "data");
          addGroup("Non-Data", buckets.nonData, "non-data");
        } else {
          appendFlatSorted();
        }
        normalizeUiTree(dtWrap);
      }
    }
  }

  // Fields table
  const tbody = $("subProfileFieldsBody");
  if (tbody) {
    tbody.innerHTML = "";
    const fields = Array.isArray(subProject.fields) ? subProject.fields : [];
      if (!fields.length) {
        const tr = document.createElement("tr");
        const td = document.createElement("td");
        td.colSpan = 7;
        td.className = "sub-profile-fields-empty";
        td.textContent = "Aucun champ \u2014 importez un fichier CI pour charger les champs.";
        tr.appendChild(td);
        tbody.appendChild(tr);
    } else {
      fields.forEach((f) => {
        const tr = document.createElement("tr");
        tr.dataset.profileSearchHay = (
          String(f.label || "") +
          " " +
          String(f.field_name || "")
        )
          .trim()
          .toLowerCase();

        const labelTd = document.createElement("td");
        labelTd.textContent = String(f.label || f.field_name || "");
        tr.appendChild(labelTd);

        const nameTd = document.createElement("td");
        nameTd.className = "field-name-cell";
        nameTd.textContent = String(f.field_name || "");
        tr.appendChild(nameTd);

        const mkBool = (val) => {
          if (val) {
            const s = document.createElement("span");
            s.className = "sub-profile-bool-yes";
            s.title = "Oui";
            return s;
          }
          const s = document.createElement("span");
          s.className = "sub-profile-bool-no";
          s.textContent = "\u2014";
          return s;
        };

        const headerTd = document.createElement("td");
        headerTd.appendChild(mkBool(f.header_field));
        tr.appendChild(headerTd);

        const prioTd = document.createElement("td");
        prioTd.textContent = f.priority_level != null ? String(f.priority_level) : "\u2014";
        tr.appendChild(prioTd);

        const threshTd = document.createElement("td");
        if (subProfilePanelState.thresholdEditMode) {
          const inp = document.createElement("input");
          inp.type = "number";
          inp.className = "sub-profile-fields-threshold-input";
          inp.min = "0";
          inp.max = "100";
          inp.step = "1";
          inp.dataset.fieldName = String(f.field_name || "");
          inp.title = "Seuil Rekey (%)";
          inp.setAttribute("aria-label", "Seuil Rekey pour " + String(f.field_name || ""));
          if (f.rekey_threshold != null && f.rekey_threshold !== "") {
            inp.value = String(f.rekey_threshold);
          } else {
            inp.value = "";
          }
          threshTd.appendChild(inp);
        } else {
          threshTd.textContent =
            f.rekey_threshold != null ? String(f.rekey_threshold) + "%" : "\u2014";
        }
        tr.appendChild(threshTd);

        const reviewTd = document.createElement("td");
        reviewTd.appendChild(mkBool(f.review_blanks));
        tr.appendChild(reviewTd);

        const dictTd = document.createElement("td");
        dictTd.className = "field-name-cell";
        dictTd.textContent = f.keying_dictionary || "\u2014";
        tr.appendChild(dictTd);

        tbody.appendChild(tr);
      });
    }
    normalizeUiTree(tbody);
    if (subProfilePanelState.thresholdEditMode) {
      _applySubProfileFieldSearchFilter();
    }
  }

  const instructionsBody = $("subProfileKeyingInstructionsBody");
  if (instructionsBody) {
    instructionsBody.innerHTML = "";
    const rows = Array.isArray(subProject.keying_instructions)
      ? subProject.keying_instructions
      : [];
    if (!rows.length) {
      const tr = document.createElement("tr");
      const td = document.createElement("td");
      td.colSpan = 2;
      td.className = "sub-profile-fields-empty";
      td.textContent = "Aucune instruction de saisie importée.";
      tr.appendChild(td);
      instructionsBody.appendChild(tr);
    } else {
      rows.forEach((item) => {
        const tr = document.createElement("tr");

        const labelTd = document.createElement("td");
        labelTd.className = "sub-profile-keying-label";
        labelTd.textContent = String(item.field_label || item.label || item.field_name || "");
        tr.appendChild(labelTd);

        const instructionsTd = document.createElement("td");
        instructionsTd.className = "sub-profile-keying-text";
        instructionsTd.textContent = String(item.keying_instructions || "");
        tr.appendChild(instructionsTd);

        instructionsBody.appendChild(tr);
      });
    }
    normalizeUiTree(instructionsBody);
  }
  renderSubProjectCiProfile(subProject);
}


export function setDocTypeOptions(docTypes, preferredDocType, docTypeGroups) {
  const sel = $("docTypeSelect");
  if (!sel) return;
  const previous = state.docType || sel.value;
  const values = Array.isArray(docTypes) && docTypes.length ? docTypes : ["Census"];
  sel.innerHTML = "";

  const appendOption = (parent, v) => {
    const o = document.createElement("option");
    o.value = String(v);
    o.textContent = String(v);
    parent.appendChild(o);
  };

  const dataGroup = Array.isArray(docTypeGroups && docTypeGroups.data) ? docTypeGroups.data : [];
  const nonDataGroup = Array.isArray(docTypeGroups && docTypeGroups.non_data) ? docTypeGroups.non_data : [];
  const hasGrouping = dataGroup.length || nonDataGroup.length;

  if (hasGrouping) {
    if (dataGroup.length) {
      const g = document.createElement("optgroup");
      g.label = "Data";
      dataGroup.forEach((v) => appendOption(g, v));
      sel.appendChild(g);
    }
    if (nonDataGroup.length) {
      const g = document.createElement("optgroup");
      g.label = "Non-Data";
      nonDataGroup.forEach((v) => appendOption(g, v));
      sel.appendChild(g);
    }
  } else {
    values.forEach((v) => appendOption(sel, v));
  }

  if (preferredDocType && values.includes(preferredDocType)) {
    sel.value = preferredDocType;
  } else if (previous && values.includes(previous)) {
    sel.value = previous;
  } else {
    sel.value = values[0];
  }
  state.docType = sel.value;
}


export function parseConf(v) {
  if (v === "" || v == null) return null;
  const s = String(v).trim();
  const isPercentDisplay = s.endsWith("%");
  const n = parseFloat(s.replace(/%/g, "").replace(",", "."));
  if (!Number.isFinite(n)) return null;
  // Display values like "1.0%" must not be parsed as 1.0 (100% ratio).
  if (isPercentDisplay) return n / 100;
  return n;
}


export function toRatio(n) {
  if (!Number.isFinite(n)) return null;
  return n > 1 ? n / 100 : n;
}

/**
 * Affichage confiance : au plus 3 chiffres aprÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â¨s la virgule (tronquÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©s), ex. 0.6, 0.768, 0.
 */

export function formatConfDisplay(raw) {
  const s = raw == null ? "" : String(raw).trim();
  if (s === "") return "";
  const n = parseFloat(s.replace(",", "."));
  if (!Number.isFinite(n)) return s;
  const truncated = Math.trunc(n * 1000) / 1000;
  if (truncated === 0) return "0";
  return String(parseFloat(truncated.toFixed(3)));
}

/**
 * Affichage conf en pourcentage : 0.8234 -> 82.3%
 */

export function formatConfPercentDisplay(raw) {
  const n = parseConf(raw);
  if (n === null) return raw == null ? "" : String(raw).trim();
  const ratio = toRatio(n);
  if (ratio === null) return String(raw);
  const pct = Math.trunc(ratio * 1000) / 10;
  if (pct === 0) return "0%";
  return `${pct.toFixed(1)}%`;
}


export function formatThresholdDisplay(th) {
  if (th == null) return "";
  const n = typeof th === "number" ? th : parseFloat(String(th).replace(",", "."));
  if (!Number.isFinite(n)) return String(th);
  const t = Math.trunc(n * 1000) / 1000;
  if (t === 0) return "0";
  return String(parseFloat(t.toFixed(3)));
}


export function prioritizeRecordEventTypeField(bases, fieldsObj) {
  const ordered = Array.isArray(bases) ? bases.slice() : [];
  if (!fieldsObj || typeof fieldsObj !== "object") return ordered;
  if (!Object.prototype.hasOwnProperty.call(fieldsObj, "RecordType")) return ordered;
  const withoutEventType = ordered.filter((base) => !isEventTypeFieldBase(base));
  return ["RecordType", ...withoutEventType];
}


export function cloneRecordSnapshot(rec) {
  if (!rec) return null;
  return JSON.parse(JSON.stringify(rec));
}

/** Copie les valeurs saisies dans le DOM vers state.work.records[].fields (ÃƒÂ©vite perte au re-render). */

export function findMoveTargetIndex(index, direction) {
  if (!state.work || !Array.isArray(state.work.records)) return -1;
  const records = state.work.records;
  if (index < 0 || index >= records.length) return -1;
  const rec = records[index];
  const headerId = rec && rec.header_id != null ? rec.header_id : null;
  if (headerId == null) return -1;
  if (direction === "up") {
    for (let i = index - 1; i >= 0; i -= 1) {
      if (records[i] && records[i].header_id === headerId) return i;
    }
    return -1;
  }
  if (direction === "down") {
    for (let i = index + 1; i < records.length; i += 1) {
      if (records[i] && records[i].header_id === headerId) return i;
    }
    return -1;
  }
  return -1;
}


export const fi = $("floatingInput");
if (fi) {
  fi.readOnly = true;
  fi.tabIndex = -1;
  fi.setAttribute("aria-hidden", "true");
  fi.addEventListener("mousedown", (e) => {
    e.preventDefault();
    if (runtime.activeEditInput && typeof runtime.activeEditInput.focus === "function") {
      runtime.activeEditInput.focus();
    }
  });
  fi.addEventListener("input", (e) => {
    if (runtime.activeEditInput && shouldForceUppercase(runtime.activeEditInput)) {
      const upVal = e.target.value.toUpperCase();
      if (e.target.value !== upVal) {
        e.target.value = upVal;
      }
    }
    adjustFloatingInputFontSize();
    // ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â°viter la boucle infinie de synchronisation
    if (runtime.activeEditInput && runtime.activeEditInput.value !== e.target.value) {
      runtime.activeEditInput.value = e.target.value;
      // DÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©clencher l'ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©vÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©nement pour la sauvegarde automatique ou d'autres ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©couteurs
      runtime.activeEditInput.dispatchEvent(new Event("input", { bubbles: true }));
    }
  });

  // Optionnel : un petit fix pour empÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Âªcher le scroll naturel du canvas si on focus le flottant alors qu'il est hors vue
  fi.addEventListener("focus", (e) => {
     // La vue est dÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©jÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â  gÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©rÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©e par notre auto-pan !
  });
}

// Synchro depuis le champ ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©dit vers le champ flottant
document.addEventListener("input", (e) => {
  const isEditInput = e.target.classList && e.target.classList.contains("field-table__input") && !e.target.readOnly;
  if (isEditInput) {
    if (shouldForceUppercase(e.target)) {
      const upVal = e.target.value.toUpperCase();
      if (e.target.value !== upVal) {
        e.target.value = upVal;
      }
    }
    if (runtime.activeEditInput === e.target) {
      const fl = $("floatingInput");
      if (fl && fl.style.display !== "none" && fl.value !== e.target.value) {
        fl.value = e.target.value;
        adjustFloatingInputFontSize();
      }
    }
  }
});


export function prefetchWorkForImage(imageId) {
  if (!state.batch || imageId == null) return;
  const key = workPrefetchKey(state.batch, imageId);
  if (prefetchCache[key] || getCachedWorkPayload(state.batch, imageId)) return;

  const controller = new AbortController();
  prefetchControllers.set(key, controller);

  const p = api(`/api/batch/${encodeURIComponent(state.batch)}/work/${imageId}`, {
    signal: controller.signal,
  });

  prefetchCache[key] = p;

  p.then((w) => {
    prefetchControllers.delete(key);
    if (w && w.image_file_url) {
      const img = new Image();
      img.src = imageViewUrl(w.image_file_url);
      // Also warm the provisional (no `v=`) URL used for cold image-switch paints.
      const provisional = new Image();
      provisional.src =
        `/api/image?batch=${encodeURIComponent(String(state.batch || ""))}` +
        `&id=${encodeURIComponent(String(imageId))}`;
    }
    if (w) {
      setCachedWorkPayload(state.batch, imageId, w);
    }
    return w;
  }).catch((e) => {
    prefetchControllers.delete(key);
    delete prefetchCache[key];
  });
}

export function prefetchNeighborWork(imageId) {
  if (!state.batch || !Array.isArray(state.images) || !state.images.length) return;
  const currIdx = state.images.findIndex((im) => im.image_id === imageId);
  if (currIdx === -1) return;
  if (currIdx > 0) {
    prefetchWorkForImage(state.images[currIdx - 1].image_id);
  }
  if (currIdx + 1 < state.images.length) {
    prefetchWorkForImage(state.images[currIdx + 1].image_id);
  }
}

export function prefetchNextWork(nextId) {
  prefetchWorkForImage(nextId);
}


export const imageListContextMenu = { root: null, imageId: null };


export function hideImageListContextMenu() {
  if (imageListContextMenu.root) {
    imageListContextMenu.root.hidden = true;
  }
  imageListContextMenu.imageId = null;
}


export function ensureImageListContextMenu() {
  if (imageListContextMenu.root) return imageListContextMenu.root;
  const root = document.createElement("div");
  root.id = "imageListContextMenu";
  root.className = "image-list-context-menu";
  root.hidden = true;
  root.setAttribute("role", "menu");
  const item = document.createElement("button");
  item.type = "button";
  item.className = "image-list-context-menu__item";
  item.setAttribute("role", "menuitem");
  item.textContent = "Ouvrir dans l'Explorateur";
  item.addEventListener("click", () => {
    const id = imageListContextMenu.imageId;
    hideImageListContextMenu();
    if (id == null || !state.batch) return;
    void (async () => {
      try {
        await api(`/api/batch/${encodeURIComponent(state.batch)}/image/${encodeURIComponent(id)}/reveal`, {
          method: "POST",
          body: "{}",
        });
      } catch (err) {
        setStatus(err.message || String(err), true);
      }
    })();
  });
  root.appendChild(item);
  document.body.appendChild(root);
  imageListContextMenu.root = root;
  return root;
}


export const zoomRange = $("zoomRange");
if (zoomRange) {
  zoomRange.addEventListener("input", () => {
    const next = parseInt(zoomRange.value, 10) || 100;
    const wrap = $("imgWrap");
    const panEl = $("imgPan");
    if (wrap && panEl && !panEl.hidden) {
      const r = wrap.getBoundingClientRect();
      zoomTowardClientPoint(next, r.left + r.width / 2, r.top + r.height / 2);
    } else {
      state.zoom = next;
      applyImageTransform();
    }
  });
}


export const filterEmptyOnly = $("filterEmptyOnly");
if (filterEmptyOnly) {
  filterEmptyOnly.addEventListener("change", applyEmptyFilter);
}


export const formContent = $("formContent");
if (formContent) {
  formContent.addEventListener("input", (e) => {
    const t = e.target;
    if (!t.matches || !t.matches(".field-table__input[data-col$='_orig']")) return;
    const tr = t.closest("tr.field-table__row");
    if (!tr) return;
    tr.dataset.allEmpty = rowAllEmptyFromInputs(tr) ? "1" : "0";
    if ($("filterEmptyOnly") && $("filterEmptyOnly").checked) applyEmptyFilter();

    beginHistoryEditSession(t);
    scheduleSuggestionsForInput(t);

    if (isOrigInputSkippable(t)) {
      const next = findNextNonSkippableOrigInput(t, 1);
      if (next && next !== t) {
        commitOrigFieldImmediately(t);
        next.focus();
      }
    }
  });

  formContent.addEventListener("focusin", (e) => {
    const t = e.target;
    if (!t.matches || !t.matches(".field-table__input[data-col$='_orig']")) return;
    beginHistoryEditSession(t);
    scheduleSuggestionsForInput(t);
  });

  formContent.addEventListener("focusout", (e) => {
    const t = e.target;
    if (!t.matches || !t.matches(".field-table__input[data-col$='_orig']")) return;
    const related = e.relatedTarget;
    const sRoot = suggestState.root;
    if (related && sRoot && sRoot.contains(related)) return;
    if (related && related.closest && related.closest(".field-suggest")) return;

    const fi = $("floatingInput");
    if (related === fi) return;

    setTimeout(() => {
      if (document.activeElement === t) return;

      hideSuggestions();

      commitOrigFieldImmediately(t);
    }, 0);
  });

  formContent.addEventListener("change", (e) => {
    const t = e.target;
    if (!t || !t.matches || !t.matches("select.field-table__input[data-col$='_orig']")) return;
    if (!isOrigInputPersistable(t)) return;
    const tr = t.closest("tr.field-table__row");
    if (tr) {
      tr.dataset.allEmpty = rowAllEmptyFromInputs(tr) ? "1" : "0";
    }
    if ($("filterEmptyOnly") && $("filterEmptyOnly").checked) applyEmptyFilter();
    commitHistoryActionForInput(t);
    beginHistoryEditSession(t);
    queueAutosaveForInput(t, false);
    const changedCol = String((t.dataset && t.dataset.col) || "").trim().toLowerCase();
    if (changedCol === "recordtype_orig") {
      syncRecordFieldsFromDom();
      renderRecordsBlock();
      primeOrigInputs($("recordsBlock"));
      applyEmptyFilter();
      void (async () => {
        if (!state.batch || !state.work || state.work.image_id == null) return;
        const mixedDocTypeChoice = await maybePromptMixedDocTypeOnLoad(
          state.work,
          state.work.image_id,
          state.batch,
          { bypassDismiss: true }
        );
        if (mixedDocTypeChoice) {
          await updateCurrentImageDocType(mixedDocTypeChoice, {
            requireConfirm: false,
            pushToHistory: true,
          });
        }
      })();
    }
    hideSuggestions();
  });
}

export const formPaneScroll = document.querySelector(".form-pane__scroll");
if (formPaneScroll) {
  formPaneScroll.addEventListener("scroll", () => {
    refreshActiveOrigSnippet();
    if (suggestState.activeInput && suggestState.root && !suggestState.root.hidden) {
      positionSuggestionsForInput(suggestState.activeInput);
    }
  });
}


export const recordsBlock = $("recordsBlock");
if (recordsBlock) {
  recordsBlock.addEventListener("click", (e) => {
    const target = e.target && e.target.closest ? e.target.closest("button[data-action]") : null;
    if (!target) return;
    const action = String(target.dataset.action || "");
    if (action === "add-record") {
      const insertIndex = parseInt(target.dataset.insertIndex || "", 10);
      if (Number.isFinite(insertIndex)) {
        void handleRecordAddClick(insertIndex);
      }
      return;
    }
    if (action === "remove-record") {
      const recordIndex = parseInt(target.dataset.recordIndex || "", 10);
      if (Number.isFinite(recordIndex)) {
        void handleRecordRemoveClick(recordIndex);
      }
      return;
    }
    if (action === "move-record-up") {
      const recordIndex = parseInt(target.dataset.recordIndex || "", 10);
      if (Number.isFinite(recordIndex)) {
        void handleRecordMoveClick(recordIndex, "up");
      }
      return;
    }
    if (action === "move-record-down") {
      const recordIndex = parseInt(target.dataset.recordIndex || "", 10);
      if (Number.isFinite(recordIndex)) {
        void handleRecordMoveClick(recordIndex, "down");
      }
      return;
    }
    if (action === "transfer-record-to-prev-image") {
      const recordIndex = parseInt(target.dataset.recordIndex || "", 10);
      if (Number.isFinite(recordIndex)) {
        void handleRecordTransferToPrevImage(recordIndex, "move");
      }
      return;
    }
    if (action === "merge-record-to-prev-image") {
      const recordIndex = parseInt(target.dataset.recordIndex || "", 10);
      if (Number.isFinite(recordIndex)) {
        void handleRecordTransferToPrevImage(recordIndex, "merge");
      }
    }
  });
  recordsBlock.addEventListener("change", (e) => {
    const t = e.target;
    if (!t || !t.matches || !t.matches("select.record-header-sel")) return;
    const recordIndex = parseInt(t.dataset.recordIndex || "", 10);
    const newHeaderId = parseInt(t.value || "", 10);
    if (!Number.isFinite(recordIndex) || !Number.isFinite(newHeaderId)) return;
    void handleRecordAssignHeader(recordIndex, newHeaderId);
  });
}


export const headerFieldsRoot = $("headerFields");
if (headerFieldsRoot) {
  headerFieldsRoot.addEventListener("click", (e) => {
    const target = e.target && e.target.closest ? e.target.closest("button[data-action]") : null;
    if (!target) return;
    const action = String(target.dataset.action || "");
    if (action === "header-move-up") {
      const idx = parseInt(target.dataset.headerIndex || "", 10);
      if (Number.isFinite(idx)) void handleHeaderMoveClick(idx, "up");
      return;
    }
    if (action === "header-move-down") {
      const idx = parseInt(target.dataset.headerIndex || "", 10);
      if (Number.isFinite(idx)) void handleHeaderMoveClick(idx, "down");
      return;
    }
    if (action === "header-delete") {
      const hid = parseInt(target.dataset.headerId || "", 10);
      if (Number.isFinite(hid)) void handleHeaderDeleteClick(hid);
    }
  });
  headerFieldsRoot.addEventListener("change", (e) => {
    const t = e.target;
    if (!t || !t.matches || !t.matches("select.header-formtype-select")) return;
    const headerId = parseInt(t.dataset.headerId || "", 10);
    if (!Number.isFinite(headerId)) return;
    void handleHeaderFormTypeChange(headerId, t.value, t);
  });
}

window.addEventListener("resize", () => {
  applyLayoutColumns();
  requestAnimationFrame(applyFieldTableColumnWidths);
  refreshActiveOrigSnippet();
  if (suggestState.activeInput && suggestState.root && !suggestState.root.hidden) {
    positionSuggestionsForInput(suggestState.activeInput);
  }
});

window.addEventListener("beforeunload", () => {
  hideSuggestions();
  flushAutosaveWithBeacon();
});

document.addEventListener("mousedown", (e) => {
  const target = e.target;
  const ctx = imageListContextMenu.root;
  if (
    ctx &&
    !ctx.hidden &&
    !ctx.contains(target) &&
    !(e.button === 2 && target.closest && target.closest(".img-list button"))
  ) {
    hideImageListContextMenu();
  }
  const root = suggestState.root;
  if (target.closest && target.closest(".field-suggest")) return;
  if (root && root.contains(target)) return;
  if (suggestState.activeInput && target === suggestState.activeInput) return;
  hideSuggestions();
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && imageListContextMenu.root && !imageListContextMenu.root.hidden) {
    e.preventDefault();
    hideImageListContextMenu();
    return;
  }
  if (e.key === "Escape" && isShortcutsPanelOpen()) {
    e.preventDefault();
    closeShortcutsPanel();
    return;
  }
  if (e.key === "Escape" && isImporterPanelOpen()) {
    e.preventDefault();
    closeImporterPanel();
    return;
  }
  if (e.key === "Escape" && runtime.typeToConfirmResolver) {
    e.preventDefault();
    resolveTypeToConfirm(false);
    return;
  }
  if (e.key === "Escape" && state.settingsOpen) {
    e.preventDefault();
    closeSettingsPanel("escape");
    return;
  }
  if (eventMatchesShortcutBinding(e, "openSettings")) {
    e.preventDefault();
    void openSettingsPanel();
    return;
  }
  if (eventMatchesShortcutBinding(e, "openFolder")) {
    e.preventDefault();
    void openFolder();
    return;
  }
  if (eventMatchesShortcutBinding(e, "closeFolder")) {
    e.preventDefault();
    void closeFolder();
    return;
  }
  if (eventMatchesShortcutBinding(e, "openImporter")) {
    e.preventDefault();
    openImporterPanel();
    return;
  }
  if (e.ctrlKey && !e.altKey) {
    const target = e.target;
    const inEditorField = !!(target && (target.classList && target.classList.contains("field-table__input")));
    const docTypeEl = $("docTypeSelect");
    const inDocTypeControl = !!(docTypeEl && target === docTypeEl);
    const inFormPanel = !!(target && target.closest && target.closest("#formContent"));
    if (!inEditorField && !inFormPanel && !inDocTypeControl) {
      // Laisse les autres zones de saisie utiliser leur comportement natif.
    } else {
      const key = String(e.key || "").toLowerCase();
      if (key === "z" && !e.shiftKey) {
        e.preventDefault();
        void undoLastEdit();
        return;
      }
      if (key === "y" || (key === "z" && e.shiftKey)) {
        e.preventDefault();
        void redoLastEdit();
        return;
      }
    }
  }
  if (e.altKey && !e.shiftKey && e.key === "ArrowLeft") {
    e.preventDefault();
    stepImage(-1);
  }
  if (e.altKey && e.shiftKey && e.key === "ArrowRight") {
    e.preventDefault();
    toggleImageViewerTab();
    return;
  }
  if (e.altKey && !e.shiftKey && String(e.key || "").toLowerCase() === "v") {
    e.preventDefault();
    toggleImageViewerTab();
    return;
  }
  if (e.altKey && !e.shiftKey && e.key === "ArrowRight") {
    e.preventDefault();
    stepImage(1);
    return;
  }
  if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) {
    const key = String(e.key || "");
    const lowerKey = key.toLowerCase();
    if (key === "+" || key === "=" || lowerKey === "add" || key === "NumpadAdd" || lowerKey === "ç") {
      if (adjustImageZoom(1)) e.preventDefault();
      return;
    }
    if (key === "-" || lowerKey === "subtract" || key === "NumpadSubtract" || lowerKey === "à") {
      if (adjustImageZoom(-1)) e.preventDefault();
      return;
    }
    if (key === "ArrowLeft") {
      if (panImage(-1, 0)) e.preventDefault();
      return;
    }
    if (key === "ArrowRight") {
      if (panImage(1, 0)) e.preventDefault();
      return;
    }
    if (key === "ArrowUp") {
      if (panImage(0, -1)) e.preventDefault();
      return;
    }
    if (key === "ArrowDown") {
      if (panImage(0, 1)) e.preventDefault();
      return;
    }
  }
});

if ($("btnPrev")) $("btnPrev").addEventListener("click", () => stepImage(-1));
if ($("btnNext")) $("btnNext").addEventListener("click", () => stepImage(1));
if ($("btnThemeToggle")) {
  $("btnThemeToggle").addEventListener("click", () => {
    const cur = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
    applyTheme(cur === "dark" ? "light" : "dark");
  });
}
if ($("btnThemeLight")) {
  $("btnThemeLight").addEventListener("click", () => applyTheme("light"));
}
if ($("btnThemeDark")) {
  $("btnThemeDark").addEventListener("click", () => applyTheme("dark"));
}
if ($("btnThemeSystem")) {
  $("btnThemeSystem").addEventListener("click", () => applyTheme("system"));
}
if ($("menuFileOpenNew")) {
  $("menuFileOpenNew").addEventListener("click", () => {
    const root = $("menuFile");
    const toggle = $("menuFileToggle");
    if (root) root.classList.remove("is-open");
    if (toggle) toggle.setAttribute("aria-expanded", "false");
    void openFolder();
  });
}
if ($("menuFileCloseFolder")) {
  $("menuFileCloseFolder").addEventListener("click", () => {
    const root = $("menuFile");
    const toggle = $("menuFileToggle");
    if (root) root.classList.remove("is-open");
    if (toggle) toggle.setAttribute("aria-expanded", "false");
    void closeFolder();
  });
}
if ($("btnUndo")) $("btnUndo").addEventListener("click", () => void undoLastEdit());
if ($("btnRedo")) $("btnRedo").addEventListener("click", () => void redoLastEdit());
if ($("btnApplyDictionaryCorrections")) {
  $("btnApplyDictionaryCorrections").addEventListener("click", async () => {
    const btn = $("btnApplyDictionaryCorrections");
    const mod = await import('../services/dictionary-autocorrect.js');
    const status = await mod.fetchAiAutocorrectStatus();
    const choice = await showAutocorrectChoicePanel(status);
    if (!choice) return;
    const isAi = choice === "ai";
    const ok = await showConfirmActionPanel(
      isAi ? mod.AI_AUTOCORRECT_CONFIRM_TITLE : mod.DICT_AUTOCORRECT_CONFIRM_TITLE,
      isAi ? mod.buildAiAutocorrectConfirmMessage(state.batch) : mod.buildDictAutocorrectConfirmMessage(state.batch),
      "Lancer Auto-Correct",
      { cancelLabel: "Annuler", kind: "default" }
    );
    if (!ok) return;
    btn.disabled = true;
    try {
      if (isAi) {
        await mod.applyAiCorrections({ source: "button" });
      } else {
        await mod.applyDictionaryCorrections({ source: "button" });
      }
    } finally {
      btn.disabled = !(state.batch && state.work);
    }
  });
}
if ($("btnAiPageFill")) {
  $("btnAiPageFill").addEventListener("click", async () => {
    const btn = $("btnAiPageFill");
    const mod = await import('../services/dictionary-autocorrect.js');
    const ok = await showConfirmActionPanel(
      mod.AI_PAGE_FILL_CONFIRM_TITLE,
      mod.buildAiPageFillConfirmMessage(state.batch),
      "Lancer AI Page Fill",
      { cancelLabel: "Annuler", kind: "default" }
    );
    if (!ok) return;
    btn.disabled = true;
    try {
      await mod.applyAiPageFill({ source: "button" });
    } finally {
      btn.disabled = !(state.batch && state.work);
    }
  });
}
if ($("btnAddHeaderSection")) {
  $("btnAddHeaderSection").addEventListener("click", async () => {
    const activeDocType = String((state.work && state.work.form_type) || state.docType || "");
    const isCensusMixed =
      resolveFieldConfigRootKind(getCurrentProjectKind()) === "Census"
      && normalizeDocTypeKey(activeDocType) === "mixed";
    const confirmTitle = isCensusMixed ? "Ajouter une section Census" : "Ajouter un Header";
    const confirmMessage = isCensusMixed
      ? "Ajouter une nouvelle section Census pour cette image Mixed ? Le Coverpage reste la derniÃ¨re section."
      : "Ajouter une nouvelle section Header pour cette image ?";
    await handleHeaderAddSection({
      confirmTitle,
      confirmMessage,
      okLabel: "Ajouter",
      statusMessage: "Ajout d'un Header...",
    });
  });
}
if ($("btnAddBdmHeaderSection")) {
  $("btnAddBdmHeaderSection").addEventListener("click", async () => {
    const isBdmMixed = isBdmMixedDocTypeActive();
    if (!isBdmMixed) return;
    await handleHeaderAddSection({
      confirmTitle: "Ajouter une section BDM",
      confirmMessage: "Ajouter une nouvelle section BDM pour cette image Mixed ?",
      okLabel: "Ajouter",
      statusMessage: "Ajout d'un Header BDM...",
    });
  });
}
if ($("btnRectFlash")) {
  $("btnRectFlash").addEventListener("change", () => {
    applyRectFlashMode(!!$("btnRectFlash").checked);
  });
}
if ($("btnRectAutoZoom")) {
  $("btnRectAutoZoom").addEventListener("change", () => {
    applyRectAutoZoomMode(!!$("btnRectAutoZoom").checked);
  });
}
if ($("btnSnippetToggle")) {
  $("btnSnippetToggle").addEventListener("change", () => {
    applySnippetToggleMode(!!$("btnSnippetToggle").checked);
  });
}
if ($("menuSettingsOpen")) {
  $("menuSettingsOpen").addEventListener("click", () => {
    const root = $("menuFile");
    const toggle = $("menuFileToggle");
    if (root) root.classList.remove("is-open");
    if (toggle) toggle.setAttribute("aria-expanded", "false");
    void openSettingsPanel();
  });
}
if ($("menuImporterOpen")) {
  $("menuImporterOpen").addEventListener("click", () => {
    const root = $("menuFile");
    const toggle = $("menuFileToggle");
    if (root) root.classList.remove("is-open");
    if (toggle) toggle.setAttribute("aria-expanded", "false");
    openImporterPanel();
  });
}
if ($("btnImporterClose")) {
  $("btnImporterClose").addEventListener("click", () => {
    closeImporterPanel();
  });
}
if ($("importerPanelBackdrop")) {
  $("importerPanelBackdrop").addEventListener("click", () => {
    closeImporterPanel();
  });
}
if ($("importerDropzone")) {
  const dz = $("importerDropzone");
  const input = $("importerFileInput");
  dz.addEventListener("click", () => {
    if (input) input.click();
  });
  dz.addEventListener("dragover", (e) => {
    e.preventDefault();
    dz.classList.add("is-dragover");
  });
  dz.addEventListener("dragleave", () => {
    dz.classList.remove("is-dragover");
  });
  dz.addEventListener("drop", (e) => {
    e.preventDefault();
    dz.classList.remove("is-dragover");
    const dt = e.dataTransfer;
    if (!dt || !dt.files) return;
    void parseDictionaryFiles(dt.files);
  });
  if (input) {
    input.addEventListener("change", () => {
      if (!input.files || !input.files.length) return;
      void parseDictionaryFiles(input.files);
      input.value = "";
    });
  }
}
if ($("btnImporterRun")) {
  $("btnImporterRun").addEventListener("click", () => {
    void confirmDictionaryImport();
  });
}
if ($("btnSettingsClose")) {
  $("btnSettingsClose").addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    closeSettingsPanel("button");
  });
}
if ($("btnConfirmActionOk")) {
  $("btnConfirmActionOk").addEventListener("click", () => resolveConfirmAction(true));
}
if ($("btnConfirmActionCancel")) {
  $("btnConfirmActionCancel").addEventListener("click", () => resolveConfirmAction(false));
}
if ($("btnConfirmActionClose")) {
  $("btnConfirmActionClose").addEventListener("click", () => resolveConfirmAction(false));
}
if ($("confirmActionBackdrop")) {
  $("confirmActionBackdrop").addEventListener("click", () => resolveConfirmAction(false));
}
if ($("btnTypeToConfirmOk")) {
  $("btnTypeToConfirmOk").addEventListener("click", () => {
    syncTypeToConfirmOkEnabled();
    const okBtn = $("btnTypeToConfirmOk");
    if (okBtn && okBtn.disabled) return;
    resolveTypeToConfirm(true);
  });
}
if ($("btnTypeToConfirmCancel")) {
  $("btnTypeToConfirmCancel").addEventListener("click", () => {
    resolveTypeToConfirm(false);
  });
}
if ($("typeToConfirmBackdrop")) {
  $("typeToConfirmBackdrop").addEventListener("click", () => {
    resolveTypeToConfirm(false);
  });
}
if ($("typeToConfirmInput")) {
  $("typeToConfirmInput").addEventListener("input", () => {
    syncTypeToConfirmOkEnabled();
  });
  $("typeToConfirmInput").addEventListener("keydown", (e) => {
    if (e.key !== "Enter") return;
    e.preventDefault();
    syncTypeToConfirmOkEnabled();
    const okBtn = $("btnTypeToConfirmOk");
    if (okBtn && !okBtn.disabled) resolveTypeToConfirm(true);
  });
}
if ($("btnAutocorrectChoiceDictionary")) {
  $("btnAutocorrectChoiceDictionary").addEventListener("click", () => resolveAutocorrectChoice("dictionary"));
}
if ($("btnAutocorrectChoiceAi")) {
  $("btnAutocorrectChoiceAi").addEventListener("click", () => resolveAutocorrectChoice("ai"));
}
if ($("btnAutocorrectChoiceCancel")) {
  $("btnAutocorrectChoiceCancel").addEventListener("click", () => resolveAutocorrectChoice(null));
}
if ($("autocorrectChoiceBackdrop")) {
  $("autocorrectChoiceBackdrop").addEventListener("click", () => resolveAutocorrectChoice(null));
}
if ($("btnLotAssignmentAccept")) {
  $("btnLotAssignmentAccept").addEventListener("click", () => {
    const selectEl = $("lotAssignmentSelect");
    const selected = selectEl ? String(selectEl.value || "").trim() : "";
    const neverShowEl = $("lotAssignmentNeverShow");
    if (neverShowEl && neverShowEl.checked) {
      state.showLotAssignmentPrompt = false;
      saveShowLotAssignmentPromptPreference();
      syncShowLotAssignmentPromptUi();
    }
    resolveLotAssignment({ action: "accept", subProject: selected });
  });
}
if ($("lotAssignmentBackdrop")) {
  $("lotAssignmentBackdrop").addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
  });
}
if ($("btnSubProjectAssignCancel")) {
  $("btnSubProjectAssignCancel").addEventListener("click", () => {
    closeSubProjectAssignPanel();
  });
}
if ($("subProjectAssignBackdrop")) {
  $("subProjectAssignBackdrop").addEventListener("click", () => {
    closeSubProjectAssignPanel();
  });
}
if ($("btnSubProjectAssignApplyThisLot")) {
  $("btnSubProjectAssignApplyThisLot").addEventListener("click", async () => {
    await applySubProjectAssignmentFromPanel(false);
  });
}
if ($("btnSubProjectAssignApplyAllLots")) {
  $("btnSubProjectAssignApplyAllLots").addEventListener("click", async () => {
    await applySubProjectAssignmentFromPanel(true);
  });
}
document.addEventListener("keydown", (e) => {
  if (e.key !== "Escape") return;
  const subProjectPanel = $("subProjectAssignPanel");
  if (subProjectPanel && !subProjectPanel.hidden) {
    e.preventDefault();
    e.stopPropagation();
    closeSubProjectAssignPanel();
    return;
  }
  const panel = $("lotAssignmentPanel");
  if (!panel || panel.hidden) return;
  e.preventDefault();
  e.stopPropagation();
});
if ($("settingsPanel")) {
  $("settingsPanel").addEventListener("click", (e) => {
    const target = e.target;
    if (target && target.closest && target.closest("[data-settings-close='1']")) {
      e.preventDefault();
      e.stopPropagation();
      closeSettingsPanel("button");
    }
  });
}
if ($("shortcutRebindCancel")) {
  $("shortcutRebindCancel").addEventListener("click", () => endShortcutRebindSession());
}
if ($("btnResetKeyboardShortcuts")) {
  $("btnResetKeyboardShortcuts").addEventListener("click", () => {
    void showConfirmActionPanel(
      "RÃ©initialiser les raccourcis ?",
      "Les combinaisons personnalisÃ©es (ParamÃ¨tres, dossier, import dictionnaire) seront effacÃ©es et les valeurs par dÃ©faut seront rÃ©tablies.",
      "RÃ©initialiser",
      { cancelLabel: "Annuler", kind: "danger" }
    ).then((ok) => {
      if (!ok) return;
      resetKeyboardShortcutsToDefaults();
      showUiFloatingToast("Raccourcis rÃ©initialisÃ©s aux valeurs par dÃ©faut.", 3200);
    });
  });
}
if ($("btnShortcutsPanelClose")) {
  $("btnShortcutsPanelClose").addEventListener("click", () => {
    closeShortcutsPanel();
  });
}
if ($("shortcutsFloatingBackdrop")) {
  $("shortcutsFloatingBackdrop").addEventListener("click", () => {
    closeShortcutsPanel();
  });
}
if ($("optShowShortcutsOnStartup")) {
  $("optShowShortcutsOnStartup").addEventListener("change", () => {
    state.showShortcutsOnStartup = $("optShowShortcutsOnStartup").checked;
    saveShortcutsPanelStartupPreference();
  });
}
function bindMergeHwrOrigColumnToggle(el) {
  if (!el) return;
  el.addEventListener("change", () => {
    state.mergeHwrOrigColumn = !!el.checked;
    saveMergeHwrOrigColumnPreference();
    syncMergeHwrOrigColumnUi();
    void refreshCurrentWorkView();
  });
}
bindMergeHwrOrigColumnToggle($("optMergeHwrOrigColumn"));
bindMergeHwrOrigColumnToggle($("topbarOptMergeHwrOrigColumn"));

function bindShowMergedHwrTextToggle(el) {
  if (!el) return;
  el.addEventListener("change", () => {
    state.showMergedHwrText = !!el.checked;
    saveShowMergedHwrTextPreference();
    syncShowMergedHwrTextUi();
    void refreshCurrentWorkView();
  });
}
bindShowMergedHwrTextToggle($("optShowMergedHwrText"));
bindShowMergedHwrTextToggle($("topbarOptShowMergedHwrText"));

function bindShowLotAssignmentPromptToggle(el) {
  if (!el) return;
  el.addEventListener("change", () => {
    state.showLotAssignmentPrompt = !!el.checked;
    saveShowLotAssignmentPromptPreference();
    syncShowLotAssignmentPromptUi();
  });
}
bindShowLotAssignmentPromptToggle($("optShowLotAssignmentPrompt"));
if ($("optSuggestRankingMode")) {
  $("optSuggestRankingMode").addEventListener("change", () => {
    const sel = $("optSuggestRankingMode");
    const v = String(sel && sel.value ? sel.value : "hybrid");
    state.suggestRankingMode =
      v === "frequency_only" || v === "recency_only" || v === "hybrid" ? v : "hybrid";
    saveSuggestRankingModePreference();
  });
}
if ($("uiFontSizeRange")) {
  const range = $("uiFontSizeRange");
  range.addEventListener("input", () => {
    updateAppearanceFontPreviewHint();
  });
  range.addEventListener("change", () => {
    const raw = parseInt(range.value, 10);
    const pct = isFinite(raw) ? raw : 100;
    applyFontScale(pct / 100);
    syncFontSizeSettingsUi();
  });
}
document.querySelectorAll(".appearance-font-presets__btn").forEach((btn) => {
  btn.addEventListener("click", () => {
    const p = parseInt(btn.dataset.fontPreset || "", 10);
    if (!isFinite(p)) return;
    const pct = Math.min(FONT_SIZE_RANGE_MAX, Math.max(FONT_SIZE_RANGE_MIN, p));
    applyFontScale(pct / 100);
    syncFontSizeSettingsUi();
  });
});
if ($("docTypeSelect")) {
  $("docTypeSelect").addEventListener("change", () => {
    void updateCurrentImageDocType($("docTypeSelect").value);
  });
}
if ($("batchSelect")) $("batchSelect").addEventListener("change", onBatchChange);
if ($("batchSelect")) {
  const batchSelect = $("batchSelect");
  const enforceSingleBatchSelectMode = () => {
    batchSelect.multiple = false;
    batchSelect.size = 1;
  };
  batchSelect.addEventListener("focus", enforceSingleBatchSelectMode);
  batchSelect.addEventListener("mousedown", enforceSingleBatchSelectMode);
  batchSelect.addEventListener("blur", enforceSingleBatchSelectMode);
}
if ($("btnOpenFolder")) $("btnOpenFolder").addEventListener("click", openFolder);
if ($("projectRoot")) {
  $("projectRoot").addEventListener("input", updateVisibleStatusBar);
  $("projectRoot").addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      applyRootFromInput();
    }
  });
}
if ($("btnResetFieldDefaults")) {
  $("btnResetFieldDefaults").addEventListener("click", async () => {
    resetFieldSelectionsToDefaults();
    renderFieldsByKind(state.selectedFieldsKind);
    setStatus("SÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©lections rÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©initialisÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©es. Cliquez sur Save pour conserver.");
    await refreshCurrentWorkView();
  });
}
if ($("btnSetFieldDefaults")) {
  $("btnSetFieldDefaults").addEventListener("click", async () => {
    try {
      await saveCurrentSelectionsAsDefaults();
      setStatus("Nouveaux paramÃ¨tres par dÃ©faut enregistrÃ©s.");
    } catch (e) {
      setStatus(e.message, true);
    }
  });
}
if ($("btnSelectAllFields")) {
  $("btnSelectAllFields").addEventListener("click", async () => {
    const changed = selectAllFieldsForKind(state.selectedFieldsKind);
    renderFieldsByKind(state.selectedFieldsKind);
    if (changed) {
      setStatus("Tous les champs ont Ã©tÃ© sÃ©lectionnÃ©s. Cliquez sur Save pour conserver.");
      await refreshCurrentWorkView();
    } else {
      setStatus("Tous les champs sont dÃ©jÃ  sÃ©lectionnÃ©s.");
    }
  });
}
if ($("btnSaveFieldSelections")) {
  $("btnSaveFieldSelections").addEventListener("click", async () => {
    try {
      await persistFieldPreferences({ includeDefaults: false });
      const feedback = $("fieldsSaveFeedback");
      if (feedback) {
        feedback.textContent = "Saved";
      }
      setStatus("SÃ©lections des champs enregistrÃ©es.");
    } catch (e) {
      setStatus(e.message, true);
    }
  });
}
if ($("fieldsSearchInput")) {
  $("fieldsSearchInput").addEventListener("input", () => {
    state.fieldSearchQuery = $("fieldsSearchInput").value || "";
    renderFieldsByKind(state.selectedFieldsKind);
  });
}
if ($("optAutoIncrementIndividualNumber")) {
  $("optAutoIncrementIndividualNumber").addEventListener("change", () => {
    state.individualNumberAutoIncrement = $("optAutoIncrementIndividualNumber").checked;
    markFieldSelectionsDirty();
  });
}
if ($("optAutoSkipValidatedRows")) {
  $("optAutoSkipValidatedRows").addEventListener("change", () => {
    state.autoSkipValidatedRows = $("optAutoSkipValidatedRows").checked;
    writeLs(AUTO_SKIP_VALIDATED_ROWS_LS, state.autoSkipValidatedRows ? "1" : "0");
    scheduleUserPreferencesSave({ auto_skip_validated_rows: !!state.autoSkipValidatedRows });
  });
}
if ($("optAutoSkipAtFields")) {
  $("optAutoSkipAtFields").addEventListener("change", () => {
    state.autoSkipAtFields = $("optAutoSkipAtFields").checked;
    writeLs(AUTO_SKIP_AT_FIELDS_LS, state.autoSkipAtFields ? "1" : "0");
    scheduleUserPreferencesSave({ auto_skip_at_fields: !!state.autoSkipAtFields });
  });
}
initPageStructureSuggestion(
  () => {
    const subProject = getActiveSubProject();
    if (!state.batch || !state.work || state.work.image_id == null || !subProject || subProject.id == null) {
      return null;
    }
    const currentIndex = state.images.findIndex(
      (image) => String(image.image_id) === String(state.work.image_id)
    );
    const previous = currentIndex > 0 ? state.images[currentIndex - 1] : null;
    const next = currentIndex >= 0 && currentIndex + 1 < state.images.length
      ? state.images[currentIndex + 1]
      : null;
    return {
      batch: String(state.batch),
      imageId: Number(state.work.image_id),
      imageIds: state.images
        .map((image) => Number(image.image_id))
        .filter((imageId) => Number.isFinite(imageId)),
      subProjectId: Number(subProject.id),
      currentDoctype: String(state.work.form_type || state.docType || ""),
      currentHeaderCount: Array.isArray(state.work.headers) ? state.work.headers.length : 0,
      currentRecordCount: Array.isArray(state.work.records) ? state.work.records.length : 0,
      previousImageId: previous && previous.image_id != null ? Number(previous.image_id) : undefined,
      nextImageId: next && next.image_id != null ? Number(next.image_id) : undefined,
    };
  },
  async () => {
    if (state.batch) {
      clearWorkPrefetchCache();
      invalidateCachedWorkPayload(state.batch, null);
    }
    await refreshCurrentWorkView();
    setStatus("Structure et valeurs AI enregistrées.");
  }
);

