import { api } from '../core/api.js';
import { readLs, writeLs } from '../core/storage.js';
import { $, escapeHtml } from '../core/dom.js';
import { AUTOCORRECTED_FIELDS_LS } from '../config/constants.js';
import { historyState, state } from '../state/app-state.js';
import { setStatus } from '../ui/status-bar.js';
import { fieldLowConf } from '../ui/form/field-table.js';
import { isOrigInputAtSkipValue, isOrigInputProtectedFromAutofill } from '../ui/highlight.js';
import { commitHistoryActionForInput, flushAutosaveQueue, queueAutosaveForInput } from '../state/autosave.js';
import { getActiveDocTypeForUi, getActiveSubProject, getCurrentRootKind, isOrigBasePersistable, resolveFieldRekeyThreshold, resolveFieldLabel } from './project-context.js';

const CORRECTED_ROW_CLASS = "field-table__row--dict-corrected";
const DICT_TOGGLE_HWR_DATA = "dictToggleHwr";
const DICT_TOGGLE_CORRECTED_DATA = "dictToggleCorrected";
const AUTOCORRECT_PERSIST_VALUE_DATA = "autocorrectPersistValue";
const DICT_AUTOCORRECT_LOADING_TEXT =
  "Correction par dictionnaire en cours. Veuillez patienter...";
const AI_CONF_HIGH_THRESHOLD = 0.8;
const AI_CONF_LOW_THRESHOLD = 0.5;
const AI_PAGE_FILL_MAX_TARGETS = 1000;

/** Keyboard shortcut label (see KEYBOARD_SHORTCUTS). */
export const DICT_CORRECTED_TOGGLE_SHORTCUT_KEYS = "Ctrl+H";

export function registerDictCorrectedToggle(input, hwrValue, correctedValue) {
  if (!input || input.readOnly) return;
  const hwr = String(hwrValue == null ? "" : hwrValue);
  const corrected = String(correctedValue == null ? "" : correctedValue);
  if (hwr === corrected) {
    clearDictCorrectedToggle(input);
    return;
  }
  input.dataset[DICT_TOGGLE_HWR_DATA] = hwr;
  input.dataset[DICT_TOGGLE_CORRECTED_DATA] = corrected;
}

export function clearDictCorrectedToggle(input) {
  if (!input) return;
  delete input.dataset[DICT_TOGGLE_HWR_DATA];
  delete input.dataset[DICT_TOGGLE_CORRECTED_DATA];
}

export function canToggleDictCorrectedOrig(input) {
  if (!input || input.readOnly) return false;
  const hwr = input.dataset[DICT_TOGGLE_HWR_DATA];
  const corrected = input.dataset[DICT_TOGGLE_CORRECTED_DATA];
  if (hwr == null || corrected == null) return false;
  return String(hwr) !== String(corrected);
}

/**
 * Swap orig. between stored hwr and dictionary-corrected values (after Auto-Correct).
 * @returns {boolean} true when a toggle was applied
 */
export function toggleDictCorrectedOrig(input) {
  if (!canToggleDictCorrectedOrig(input)) return false;
  const hwr = String(input.dataset[DICT_TOGGLE_HWR_DATA]);
  const corrected = String(input.dataset[DICT_TOGGLE_CORRECTED_DATA]);
  const current = String(input.value || "");
  const next = current === corrected ? hwr : corrected;
  if (next === current) return false;
  input.value = next;
  input.dispatchEvent(new Event("input", { bubbles: true }));
  const row = input.closest("tr.field-table__row");
  if (next === hwr) {
    clearPersistedAutocorrectMarker(input);
    if (row) row.classList.remove(CORRECTED_ROW_CLASS);
  } else {
    persistAutocorrectMarker(input);
    if (row) row.classList.add(CORRECTED_ROW_CLASS);
  }
  return true;
}

function getSiblingHwrValue(input) {
  if (!input || !input.closest) return null;
  const col = String(input.dataset && input.dataset.col ? input.dataset.col : "");
  if (!col.endsWith("_orig")) return null;
  const base = col.slice(0, -"_orig".length);
  const row = input.closest("tr.field-table__row");
  if (!row) return null;
  const hwrInput = row.querySelector(`[data-col="${base}_hwr"]`);
  if (!hwrInput) return null;
  return String(hwrInput.value == null ? "" : hwrInput.value);
}

function setInputValue(input, value) {
  const next = String(value == null ? "" : value);
  if (input.tagName === "SELECT" && !Array.from(input.options).some((opt) => opt.value === next)) {
    const opt = document.createElement("option");
    opt.value = next;
    opt.textContent = next;
    input.insertBefore(opt, input.firstChild);
  }
  if (String(input.value || "") === next) return true;
  input.value = next;
  input.dispatchEvent(new Event("input", { bubbles: true }));
  return true;
}

/**
 * Restore an orig. field from its row hwr value.
 * Clears the Auto-correct blue marker so restored fields do not stay highlighted.
 * @returns {boolean} true when the shortcut was handled
 */
export function restoreOrigFromHwr(input) {
  if (!input || input.readOnly) return false;
  const hwr = input.dataset[DICT_TOGGLE_HWR_DATA] != null
    ? String(input.dataset[DICT_TOGGLE_HWR_DATA])
    : getSiblingHwrValue(input);
  if (hwr == null) return false;
  const ok = setInputValue(input, hwr);
  clearPersistedAutocorrectMarker(input);
  const row = input.closest ? input.closest("tr.field-table__row") : null;
  if (row) row.classList.remove(CORRECTED_ROW_CLASS);
  const meta = findWorkFieldMetaForInput(input);
  if (meta) meta.edt = null;
  return ok;
}

/** @typedef {{ id: string, label: string, status: "pending"|"active"|"done", startedAt: number|null, durationMs: number|null }} AutocorrectProgressStep */
/** @typedef {{ title: string, startedAt: number, steps: AutocorrectProgressStep[], currentIndex: number }} AutocorrectProgressTracker */

let activeProgressTracker = null;
let progressRenderTimerId = null;

/**
 * Format a millisecond duration for the autocorrect progress overlay.
 * @param {number} ms
 * @returns {string}
 */
function formatProgressDuration(ms) {
  const value = Math.max(0, ms);
  if (value < 1000) return `${Math.round(value)} ms`;
  if (value < 10000) return `${(value / 1000).toFixed(1)} s`;
  return `${Math.round(value / 1000)} s`;
}

function formatInteger(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return "0";
  return Math.max(0, Math.round(n)).toLocaleString("fr-FR");
}

function buildAiJobStats(usage, elapsedMs) {
  const raw = usage && typeof usage === "object" ? usage : {};
  return {
    inputTokens: Number(raw.input_tokens || 0),
    outputTokens: Number(raw.output_tokens || 0),
    totalTokens: Number(raw.total_tokens || 0),
    requests: Number(raw.requests || 0),
    elapsedMs: Number(elapsedMs || 0),
  };
}

function formatAiJobStatsText(stats) {
  if (!stats || typeof stats !== "object") return "";
  const parts = [
    `Durée totale : ${formatProgressDuration(stats.elapsedMs)}`,
    `Jetons d'entrée : ${formatInteger(stats.inputTokens)}`,
    `Jetons de sortie : ${formatInteger(stats.outputTokens)}`,
    `Total : ${formatInteger(stats.totalTokens)}`,
  ];
  if (Number(stats.requests) > 1) {
    parts.push(`Appels AI : ${formatInteger(stats.requests)}`);
  }
  return parts.join(" — ");
}

/**
 * Create a step tracker for dictionary/AI loading overlays.
 * @param {string} title
 * @returns {AutocorrectProgressTracker}
 */
export function createAutocorrectProgress(title) {
  return {
    title: String(title || "").trim(),
    startedAt: performance.now(),
    steps: [],
    currentIndex: -1,
  };
}

/**
 * Register one progress step before the operation starts.
 * @param {AutocorrectProgressTracker} tracker
 * @param {string} id
 * @param {string} label
 */
export function progressAddStep(tracker, id, label) {
  tracker.steps.push({
    id: String(id),
    label: String(label),
    status: "pending",
    startedAt: null,
    durationMs: null,
  });
}

/**
 * @param {AutocorrectProgressTracker} tracker
 * @param {string} id
 * @returns {AutocorrectProgressStep|null}
 */
function progressFindStep(tracker, id) {
  return tracker.steps.find((step) => step.id === id) || null;
}

/**
 * Close the currently active progress step, if any.
 * @param {AutocorrectProgressTracker} tracker
 */
function progressCloseActiveStep(tracker) {
  if (tracker.currentIndex < 0) return;
  const step = tracker.steps[tracker.currentIndex];
  if (!step || step.status !== "active") return;
  step.durationMs = performance.now() - (step.startedAt || tracker.startedAt);
  step.status = "done";
}

/**
 * Mark a step as active and render the overlay.
 * @param {AutocorrectProgressTracker} tracker
 * @param {string} id
 */
export function progressStartStep(tracker, id) {
  progressCloseActiveStep(tracker);
  const step = progressFindStep(tracker, id);
  if (!step) return;
  step.status = "active";
  step.startedAt = performance.now();
  tracker.currentIndex = tracker.steps.indexOf(step);
  renderAutocorrectProgress(tracker);
}

/**
 * Mark a step as completed and render the overlay.
 * @param {AutocorrectProgressTracker} tracker
 * @param {string} id
 */
export function progressCompleteStep(tracker, id) {
  const step = progressFindStep(tracker, id);
  if (!step) return;
  if (step.status === "active") {
    step.durationMs = performance.now() - (step.startedAt || tracker.startedAt);
  } else if (step.durationMs == null) {
    step.durationMs = 0;
  }
  step.status = "done";
  if (tracker.currentIndex === tracker.steps.indexOf(step)) {
    tracker.currentIndex = -1;
  }
  renderAutocorrectProgress(tracker);
}

/**
 * @param {AutocorrectProgressTracker} tracker
 * @returns {AutocorrectProgressStep|null}
 */
function progressSlowestStep(tracker) {
  /** @type {AutocorrectProgressStep|null} */
  let slowest = null;
  tracker.steps.forEach((step) => {
    if (step.status !== "done" || step.durationMs == null) return;
    if (!slowest || step.durationMs > slowest.durationMs) slowest = step;
  });
  return slowest;
}

/**
 * Render step list, per-step durations, and total elapsed time.
 * @param {AutocorrectProgressTracker} tracker
 */
function renderAutocorrectProgress(tracker) {
  if (!tracker) return;
  const textEl = $("dictAutocorrectOverlayText");
  const stepsEl = $("dictAutocorrectOverlaySteps");
  const totalEl = $("dictAutocorrectOverlayTotal");
  const panel = $("dictAutocorrectOverlay");
  if (textEl) textEl.textContent = tracker.title;
  if (panel) panel.classList.add("dict-autocorrect-overlay--detailed");
  const slowest = progressSlowestStep(tracker);
  const allDone =
    tracker.steps.length > 0 && tracker.steps.every((step) => step.status === "done");
  if (stepsEl) {
    stepsEl.hidden = false;
    stepsEl.innerHTML = tracker.steps
      .map((step, index) => {
        const isSlowest =
          !!slowest && step.id === slowest.id && allDone && tracker.steps.length > 1;
        let durationText = "";
        if (step.status === "done" && step.durationMs != null) {
          durationText = formatProgressDuration(step.durationMs);
        } else if (step.status === "active" && step.startedAt != null) {
          durationText = formatProgressDuration(performance.now() - step.startedAt);
        }
        const statusLabel =
          step.status === "active"
            ? "En cours"
            : step.status === "done"
              ? "Terminé"
              : "En attente";
        return (
          `<li class="dict-autocorrect-overlay__step dict-autocorrect-overlay__step--${step.status}` +
          (isSlowest ? " dict-autocorrect-overlay__step--slowest" : "") +
          `">` +
          `<span class="dict-autocorrect-overlay__step-index">${index + 1}.</span>` +
          `<span class="dict-autocorrect-overlay__step-label">${escapeHtml(step.label)}</span>` +
          `<span class="dict-autocorrect-overlay__step-meta">` +
          `<span class="dict-autocorrect-overlay__step-status">${statusLabel}</span>` +
          (durationText
            ? `<span class="dict-autocorrect-overlay__step-duration">${durationText}</span>`
            : "") +
          (isSlowest
            ? `<span class="dict-autocorrect-overlay__step-badge">Plus long</span>`
            : "") +
          `</span></li>`
        );
      })
      .join("");
  }
  const totalMs = performance.now() - tracker.startedAt;
  if (totalEl) {
    totalEl.hidden = false;
    let totalText = `Durée totale : ${formatProgressDuration(totalMs)}`;
    if (allDone && slowest && tracker.steps.length > 1) {
      totalText += ` — plus long : ${slowest.label} (${formatProgressDuration(slowest.durationMs)})`;
    }
    totalEl.textContent = totalText;
  }
}

function startProgressRenderTimer() {
  stopProgressRenderTimer();
  progressRenderTimerId = window.setInterval(() => {
    if (activeProgressTracker) renderAutocorrectProgress(activeProgressTracker);
  }, 250);
}

function stopProgressRenderTimer() {
  if (progressRenderTimerId != null) {
    window.clearInterval(progressRenderTimerId);
    progressRenderTimerId = null;
  }
}

function resetAutocorrectProgressUi() {
  stopProgressRenderTimer();
  activeProgressTracker = null;
  const stepsEl = $("dictAutocorrectOverlaySteps");
  const totalEl = $("dictAutocorrectOverlayTotal");
  const panel = $("dictAutocorrectOverlay");
  if (stepsEl) {
    stepsEl.hidden = true;
    stepsEl.innerHTML = "";
  }
  if (totalEl) {
    totalEl.hidden = true;
    totalEl.textContent = "";
  }
  if (panel) panel.classList.remove("dict-autocorrect-overlay--detailed");
}

/**
 * Show the blocking autocorrect overlay.
 * @param {string} [message]
 * @param {AutocorrectProgressTracker|null} [progressTracker]
 */
export function showDictAutocorrectLoading(message, progressTracker) {
  const text = String(message || DICT_AUTOCORRECT_LOADING_TEXT);
  const panel = $("dictAutocorrectOverlay");
  const backdrop = $("dictAutocorrectBackdrop");
  const textEl = $("dictAutocorrectOverlayText");
  resetAutocorrectProgressUi();
  if (progressTracker) {
    activeProgressTracker = progressTracker;
    renderAutocorrectProgress(progressTracker);
    startProgressRenderTimer();
  } else if (textEl) {
    textEl.textContent = text;
  }
  if (panel) {
    panel.hidden = false;
    panel.setAttribute("aria-busy", "true");
  }
  if (backdrop) backdrop.hidden = false;
}

export function hideDictAutocorrectLoading() {
  const panel = $("dictAutocorrectOverlay");
  const backdrop = $("dictAutocorrectBackdrop");
  resetAutocorrectProgressUi();
  if (panel) {
    panel.hidden = true;
    panel.setAttribute("aria-busy", "false");
  }
  if (backdrop) backdrop.hidden = true;
}

let dictAutocorrectResultsResolver = null;
let dictAutocorrectResultsBindingsReady = false;
const AI_AUTOCORRECT_REASON_LABELS = {
  "matched-page-ai": "Rempli par AI Page Fill",
  "matched-ai": "Corrigé par AI",
  "ai-not-configured": "AI non configurée",
  "ai-unavailable": "AI indisponible",
  "image-not-found": "Image introuvable",
  "missing-rect": "Coordonnées manquantes",
  "crop-failed": "Extraction de l'image impossible",
  "invalid-ai-response": "Réponse AI invalide",
};

const DICT_AUTOCORRECT_REASON_LABELS = {
  matched: "Corrigé",
  "no-match": "Pas de correspondance dans le dictionnaire général",
  "empty-input": "Valeur hwr vide",
  "field-not-resolved": "Champ non reconnu",
  unchanged: "Déjà conforme au dictionnaire (orig. inchangé)",
};

function ensureDictAutocorrectResultsBindings() {
  if (dictAutocorrectResultsBindingsReady) return;
  dictAutocorrectResultsBindingsReady = true;
  const close = () => hideDictAutocorrectResultsPanel();
  const okBtn = $("btnDictAutocorrectResultsOk");
  const backdrop = $("dictAutocorrectResultsBackdrop");
  const listEl = $("dictAutocorrectResultsList");
  if (okBtn) okBtn.addEventListener("click", close);
  if (backdrop) backdrop.addEventListener("click", close);
  if (listEl) {
    listEl.addEventListener("click", (event) => {
      const item = event.target.closest(".dict-autocorrect-results__item--link");
      if (!item || !listEl.contains(item)) return;
      navigateFromAutocorrectResultItem(item);
    });
    listEl.addEventListener("keydown", (event) => {
      if (event.key !== "Enter" && event.key !== " ") return;
      const item = event.target.closest(".dict-autocorrect-results__item--link");
      if (!item || !listEl.contains(item)) return;
      event.preventDefault();
      navigateFromAutocorrectResultItem(item);
    });
  }
}

function formatFieldLocation(target) {
  if (!target) return "";
  if (target.record_id != null) return `Record #${target.record_id}`;
  if (target.header_id != null) return `Header #${target.header_id}`;
  return "Header";
}

function formatFieldDisplayName(fieldName, target) {
  const base = String(fieldName || "").trim();
  const label = resolveFieldLabel(base, base);
  const loc = formatFieldLocation(target);
  return loc ? `${label} (${loc})` : label;
}

function describeAutocorrectReason(reason) {
  if (AI_AUTOCORRECT_REASON_LABELS[String(reason || "")]) {
    return AI_AUTOCORRECT_REASON_LABELS[String(reason || "")];
  }
  return DICT_AUTOCORRECT_REASON_LABELS[String(reason || "")] || String(reason || "—");
}

function buildAutocorrectResultItemAttrs(item) {
  const fieldName = String(item.fieldName || "").trim();
  const recordId = item.recordId != null ? String(item.recordId) : "";
  const headerId = item.headerId != null ? String(item.headerId) : "";
  return (
    ' class="dict-autocorrect-results__item dict-autocorrect-results__item--link"' +
    ' role="button" tabindex="0"' +
    ` data-field-name="${escapeHtml(fieldName)}"` +
    (recordId ? ` data-record-id="${escapeHtml(recordId)}"` : "") +
    (headerId ? ` data-header-id="${escapeHtml(headerId)}"` : "") +
    ' title="Aller au champ"'
  );
}

/**
 * Focus the orig. input for one autocorrect/page-fill result row.
 * @returns {boolean} true when the field was found and focused
 */
function focusAutocorrectResultField(entry) {
  const fieldName = String(entry.fieldName || "").trim();
  if (!fieldName) return false;
  const recordIdRaw = entry.recordId;
  const headerIdRaw = entry.headerId;
  const recordId =
    recordIdRaw != null && recordIdRaw !== "" ? parseInt(String(recordIdRaw), 10) : null;
  const headerId =
    headerIdRaw != null && headerIdRaw !== "" ? parseInt(String(headerIdRaw), 10) : null;
  const input = findOrigInput(
    fieldName,
    Number.isFinite(recordId) ? recordId : null,
    Number.isFinite(headerId) ? headerId : null
  );
  if (!input) return false;

  const card = input.closest(".record-card");
  const headerSection = input.closest(".header-section");
  const row = input.closest("tr.field-table__row");
  if (card && typeof card.scrollIntoView === "function") {
    card.scrollIntoView({ behavior: "smooth", block: "nearest" });
  } else if (headerSection && typeof headerSection.scrollIntoView === "function") {
    headerSection.scrollIntoView({ behavior: "smooth", block: "nearest" });
  }
  if (row && typeof row.scrollIntoView === "function") {
    row.scrollIntoView({ behavior: "smooth", block: "center" });
  }
  input.focus();
  return true;
}

function navigateFromAutocorrectResultItem(item) {
  if (!item) return;
  const fieldName = String(item.dataset.fieldName || "").trim();
  const recordIdRaw = item.dataset.recordId;
  const headerIdRaw = item.dataset.headerId;
  const recordId =
    recordIdRaw != null && recordIdRaw !== "" ? parseInt(recordIdRaw, 10) : null;
  const headerId =
    headerIdRaw != null && headerIdRaw !== "" ? parseInt(headerIdRaw, 10) : null;
  hideDictAutocorrectResultsPanel();
  requestAnimationFrame(() => {
    const focused = focusAutocorrectResultField({ fieldName, recordId, headerId });
    if (!focused) {
      setStatus("Champ introuvable dans le formulaire.", true);
    }
  });
}

function buildAutocorrectResultsReport(targets, results) {
  const corrected = [];
  const unchanged = [];
  const noMatch = [];

  results.forEach((res, idx) => {
    const target = targets[idx];
    if (!target) return;
    const fieldName = String((res && res.field_name) || target.field_name || "").trim();
    const hwr = String((res && res.input) != null ? res.input : target.value || "");
    const entry = {
      fieldName,
      recordId: target.record_id != null ? target.record_id : null,
      headerId: target.header_id != null ? target.header_id : null,
      displayName: formatFieldDisplayName(fieldName, target),
      hwr,
      location: formatFieldLocation(target),
    };

    if (res && res.applied && res.output != null) {
      const output = String(res.output);
      const before = String(target.input.value || "");
      if (before !== output) {
        corrected.push({
          ...entry,
          before,
          after: output,
          reason: String(res.reason || "matched"),
        });
      } else {
        unchanged.push({ ...entry, value: output, reason: "unchanged" });
      }
    } else {
      noMatch.push({
        ...entry,
        reason: String((res && res.reason) || "no-match"),
      });
    }
  });

  const total = targets.length;
  const summary =
    `${corrected.length} corrigé(s), ${unchanged.length} déjà conforme(s), ` +
    `${noMatch.length} sans correspondance — ${total} champ(s) analysé(s).`;

  return { summary, corrected, unchanged, noMatch, total };
}

function buildAutocorrectResultsListHtml(report) {
  const rows = [];
  report.corrected.forEach((item) => {
    const before = item.before ? `« ${item.before} » → ` : "";
    rows.push(
      `<li${buildAutocorrectResultItemAttrs(item)}>` +
        `<div class="dict-autocorrect-results__item-title">${escapeHtml(item.displayName)}</div>` +
        `<div class="dict-autocorrect-results__item-meta">hwr : ${escapeHtml(item.hwr)}</div>` +
        `<div class="dict-autocorrect-results__item-detail dict-autocorrect-results__item-detail--ok">` +
        `${escapeHtml(describeAutocorrectReason("matched"))} : ${before}« ${escapeHtml(item.after)} »</div></li>`
    );
  });
  report.unchanged.forEach((item) => {
    rows.push(
      `<li${buildAutocorrectResultItemAttrs(item)}>` +
        `<div class="dict-autocorrect-results__item-title">${escapeHtml(item.displayName)}</div>` +
        `<div class="dict-autocorrect-results__item-meta">hwr : ${escapeHtml(item.hwr)}</div>` +
        `<div class="dict-autocorrect-results__item-detail dict-autocorrect-results__item-detail--skip">` +
        `${escapeHtml(describeAutocorrectReason("unchanged"))} (« ${escapeHtml(item.value)} »)</div></li>`
    );
  });
  report.noMatch.forEach((item) => {
    rows.push(
      `<li${buildAutocorrectResultItemAttrs(item)}>` +
        `<div class="dict-autocorrect-results__item-title">${escapeHtml(item.displayName)}</div>` +
        `<div class="dict-autocorrect-results__item-meta">hwr : ${escapeHtml(item.hwr)}</div>` +
        `<div class="dict-autocorrect-results__item-detail dict-autocorrect-results__item-detail--skip">` +
        `${escapeHtml(describeAutocorrectReason(item.reason))}</div></li>`
    );
  });
  if (!rows.length) {
    return "<p class=\"dict-autocorrect-results__item-detail\">Aucun détail à afficher.</p>";
  }
  return `<ul>${rows.join("")}</ul>`;
}

function ensureAiJobStatsElement(summaryEl) {
  if (!summaryEl || !summaryEl.parentElement) return null;
  let statsEl = $("dictAutocorrectResultsAiStats");
  if (!statsEl) {
    statsEl = document.createElement("p");
    statsEl.id = "dictAutocorrectResultsAiStats";
    statsEl.className = "dict-autocorrect-results__ai-stats";
    summaryEl.insertAdjacentElement("afterend", statsEl);
  }
  return statsEl;
}

export function showDictAutocorrectResultsPanel(report) {
  ensureDictAutocorrectResultsBindings();
  const panel = $("dictAutocorrectResultsPanel");
  const backdrop = $("dictAutocorrectResultsBackdrop");
  const summaryEl = $("dictAutocorrectResultsSummary");
  const listEl = $("dictAutocorrectResultsList");
  if (summaryEl) summaryEl.textContent = report.summary || "";
  const statsEl = ensureAiJobStatsElement(summaryEl);
  if (statsEl) {
    const statsText = formatAiJobStatsText(report.aiStats);
    statsEl.textContent = statsText;
    statsEl.hidden = !statsText;
  }
  if (listEl) listEl.innerHTML = buildAutocorrectResultsListHtml(report);
  if (panel) panel.hidden = false;
  if (backdrop) backdrop.hidden = false;
  return new Promise((resolve) => {
    dictAutocorrectResultsResolver = resolve;
  });
}

export function hideDictAutocorrectResultsPanel() {
  const panel = $("dictAutocorrectResultsPanel");
  const backdrop = $("dictAutocorrectResultsBackdrop");
  if (panel) panel.hidden = true;
  if (backdrop) backdrop.hidden = true;
  if (dictAutocorrectResultsResolver) {
    dictAutocorrectResultsResolver();
    dictAutocorrectResultsResolver = null;
  }
}

/** Title for the confirmation panel before running Auto-Correct from the toolbar. */
export const DICT_AUTOCORRECT_CONFIRM_TITLE = "Correction automatique d'après le dictionnaire";

/** Build a short confirm message including the active lot name. */
export function buildDictAutocorrectConfirmMessage(batchName) {
  const lot = String(batchName || "").trim() || "—";
  return (
    `Voulez-vous appliquer cette correction au lot actuellement sélectionné : « ${lot} » ?\n\n` +
    "Champs hwr sous le seuil Rekey : casse et accents via le dictionnaire général (valeur hwr). " +
    "Lignes corrigées en bleu."
  );
}

export const AI_AUTOCORRECT_CONFIRM_TITLE = "Correction automatique par AI";

export function buildAiAutocorrectConfirmMessage(batchName) {
  const lot = String(batchName || "").trim() || "—";
  return (
    `Voulez-vous appliquer cette correction au lot actuellement sélectionné : « ${lot} » ?\n\n` +
    "Champs hwr sous le seuil Rekey avec coordonnées : lecture de chaque rectangle via la passerelle sécurisée Datafix. " +
    "Lignes corrigées en bleu."
  );
}

export const AI_PAGE_FILL_CONFIRM_TITLE = "Reconnaissance AI de toute la page";

export function buildAiPageFillConfirmMessage(batchName) {
  const lot = String(batchName || "").trim() || "-";
  return (
    `Voulez-vous lire toute la page du lot actuellement sélectionné : « ${lot} » ?\n\n` +
    "L'AI utilisera l'image complète, la langue, les champs visibles et les Keying Instructions. " +
    "Les valeurs retournées remplaceront les champs orig. visibles des lignes existantes."
  );
}

/**
 * Whether a single field is eligible for dictionary auto-correction.
 * Only low-confidence fields (conf below the sub-project Seuil Rekey) with a non-empty
 * hwr are corrected; orig may already be filled. Fields meeting the threshold are left as-is.
 */
export function isEligibleForDictionaryAutocorrect(base, meta, recordId) {
  const fieldBase = String(base || "").trim();
  if (!fieldBase || !meta || typeof meta !== "object") return false;
  const hwr = String(meta.hwr == null ? "" : meta.hwr).trim();
  if (hwr === "") return false;
  const rekeyThreshold = resolveFieldRekeyThreshold(fieldBase, meta);
  const rowMeta = { ...meta, threshold: rekeyThreshold };
  if (!fieldLowConf(rowMeta, meta.conf, fieldBase)) return false;
  if (!isOrigBasePersistable(fieldBase, recordId)) return false;
  return true;
}

function findOrigInput(base, recordId, headerId) {
  const sel = recordId != null
    ? `tr.field-table__row[data-record-id="${recordId}"][data-base="${base}"] input[data-col="${base}_orig"]`
    : `tr.field-table__row[data-header-id="${headerId}"][data-base="${base}"] input[data-col="${base}_orig"]`;
  const input = document.querySelector(sel);
  if (!input || input.tagName !== "INPUT" || input.readOnly) return null;
  return input;
}

function collectEligibleTargets() {
  const work = state.work;
  if (!work || typeof work !== "object") return [];
  const targets = [];

  const pushFromFields = (fields, recordId, headerId) => {
    if (!fields || typeof fields !== "object") return;
    Object.keys(fields).forEach((base) => {
      const meta = fields[base];
      if (!isEligibleForDictionaryAutocorrect(base, meta, recordId)) return;
      const input = findOrigInput(base, recordId, headerId);
      if (!input || isOrigInputAtSkipValue(input)) return;
      const row = input.closest("tr.field-table__row");
      targets.push({
        field_name: base,
        value: String(meta.hwr == null ? "" : meta.hwr),
        record_id: recordId,
        header_id: headerId,
        rect: String((row && row.dataset.rect) || meta.rect || ""),
        input,
      });
    });
  };

  const headers = Array.isArray(work.headers) && work.headers.length
    ? work.headers
    : work.header && work.header_id != null
      ? [{ header_id: work.header_id, fields: work.header }]
      : [];
  headers.forEach((hh) => {
    if (hh && hh.fields) pushFromFields(hh.fields, null, hh.header_id);
  });

  if (Array.isArray(work.records)) {
    work.records.forEach((rec) => {
      if (rec && rec.fields) pushFromFields(rec.fields, rec.record_id, rec.header_id);
    });
  }

  return targets;
}

function collectEligibleAiTargets() {
  return collectEligibleTargets().filter((target) => String(target.rect || "").trim() !== "");
}

function collectVisiblePageFillTargets() {
  const targets = [];
  const seen = new Set();
  document.querySelectorAll("#formContent tr.field-table__row").forEach((row) => {
    if (!(row instanceof HTMLElement) || row.hidden) return;
    const base = String(row.dataset.base || "").trim();
    if (!base) return;
    const input = row.querySelector(`.field-table__input[data-col="${base}_orig"]`);
    if (!input || input.readOnly) return;
    const recordIdRaw = row.dataset.recordId;
    const headerIdRaw = row.dataset.headerId || (state.work && state.work.header_id != null ? String(state.work.header_id) : "");
    const recordId = recordIdRaw != null && recordIdRaw !== "" ? parseInt(recordIdRaw, 10) : null;
    const headerId = headerIdRaw != null && headerIdRaw !== "" ? parseInt(headerIdRaw, 10) : null;
    if (recordId == null && !Number.isFinite(headerId)) return;
    if (!isOrigBasePersistable(base, Number.isFinite(recordId) ? recordId : null)) return;
    if (isOrigInputProtectedFromAutofill(input)) return;
    const key = `${Number.isFinite(recordId) ? "r:" + recordId : "h:" + headerId}:${base}`;
    if (seen.has(key)) return;
    seen.add(key);
    const labelEl = row.querySelector(".field-table__label");
    targets.push({
      field_name: base,
      field_label: labelEl ? String(labelEl.textContent || "").trim() : resolveFieldLabel(base, base),
      record_id: Number.isFinite(recordId) ? recordId : null,
      header_id: Number.isFinite(headerId) ? headerId : null,
      input,
    });
  });
  return targets;
}

function applyCorrectionToInput(input, correctedValue, hwrValue) {
  const hwr = String(hwrValue == null ? "" : hwrValue);
  const corrected = String(correctedValue == null ? "" : correctedValue);
  markAutocorrectedInput(input, corrected);
  input.value = corrected;
  input.dispatchEvent(new Event("input", { bubbles: true }));
  queueAutosaveForInput(input, false);
  registerDictCorrectedToggle(input, hwr, corrected);
}

function findWorkFieldMetaForInput(input) {
  const row = input && input.closest ? input.closest("tr.field-table__row") : null;
  if (!row || !state.work) return null;
  const base = String(row.dataset.base || "").trim();
  if (!base) return null;
  const recordIdRaw = row.dataset.recordId;
  if (recordIdRaw != null && recordIdRaw !== "" && Array.isArray(state.work.records)) {
    const rec = state.work.records.find((item) => item && String(item.record_id) === String(recordIdRaw));
    return rec && rec.fields ? rec.fields[base] || null : null;
  }
  const headerIdRaw = row.dataset.headerId || (state.work.header_id != null ? String(state.work.header_id) : "");
  const headers = Array.isArray(state.work.headers) ? state.work.headers : [];
  const header = headers.find((item) => item && String(item.header_id) === String(headerIdRaw));
  if (header && header.fields) return header.fields[base] || null;
  return state.work.header && state.work.header[base] ? state.work.header[base] : null;
}

function autocorrectMarkerKeyFromInput(input) {
  const row = input && input.closest ? input.closest("tr.field-table__row") : null;
  if (!row || !state.batch || !state.work || state.work.image_id == null) return "";
  const base = String(row.dataset.base || "").trim();
  if (!base) return "";
  const imagePart = `i:${state.work.image_id}`;
  const recordIdRaw = row.dataset.recordId;
  if (recordIdRaw != null && recordIdRaw !== "") {
    return `${state.batch}|${imagePart}|r:${recordIdRaw}|${base}`;
  }
  const headerIdRaw = row.dataset.headerId || (state.work.header_id != null ? String(state.work.header_id) : "");
  if (headerIdRaw) return `${state.batch}|${imagePart}|h:${headerIdRaw}|${base}`;
  return "";
}

function readAutocorrectMarkers() {
  try {
    return JSON.parse(readLs(AUTOCORRECTED_FIELDS_LS) || "{}") || {};
  } catch (_) {
    return {};
  }
}

function writeAutocorrectMarkers(markers) {
  writeLs(AUTOCORRECTED_FIELDS_LS, JSON.stringify(markers || {}));
}

function persistAutocorrectMarker(input) {
  const key = autocorrectMarkerKeyFromInput(input);
  if (!key) return;
  const markers = readAutocorrectMarkers();
  markers[key] = Date.now();
  writeAutocorrectMarkers(markers);
}

/**
 * Drop the browser Auto-correct marker for one orig. input (clears blue on reload).
 * @param {HTMLElement|null|undefined} input
 */
function clearPersistedAutocorrectMarker(input) {
  const key = autocorrectMarkerKeyFromInput(input);
  if (!key) return;
  const markers = readAutocorrectMarkers();
  if (!Object.prototype.hasOwnProperty.call(markers, key)) return;
  delete markers[key];
  writeAutocorrectMarkers(markers);
}

/**
 * Remap image-scoped Auto-correct markers after global RecordID renumber.
 * @param {Map<number, number>|null|undefined} idMap old RecordID -> new RecordID
 */
export function remapAutocorrectMarkersAfterRenumber(idMap) {
  if (!idMap || !state.batch || typeof idMap.get !== "function") return;
  const markers = readAutocorrectMarkers();
  const keys = Object.keys(markers);
  if (!keys.length) return;
  const batchPrefix = `${state.batch}|`;
  let changed = false;
  const next = { ...markers };
  keys.forEach((key) => {
    if (!key.startsWith(batchPrefix)) return;
    // batch|i:imageId|r:recordId|base
    const parts = key.split("|");
    if (parts.length < 4 || !String(parts[2] || "").startsWith("r:")) return;
    const oldId = parseInt(String(parts[2]).slice(2), 10);
    if (!Number.isFinite(oldId)) return;
    const newId = idMap.get(oldId);
    if (!Number.isFinite(newId) || newId === oldId) return;
    const newKey = `${parts[0]}|${parts[1]}|r:${newId}|${parts.slice(3).join("|")}`;
    next[newKey] = next[key];
    delete next[key];
    changed = true;
  });
  if (changed) writeAutocorrectMarkers(next);
}

function markAutocorrectedInput(input, value) {
  if (!input || !input.dataset) return;
  const corrected = String(value == null ? "" : value);
  input.dataset[AUTOCORRECT_PERSIST_VALUE_DATA] = corrected;
  persistAutocorrectMarker(input);
  const row = input.closest("tr.field-table__row");
  if (row) row.classList.add(CORRECTED_ROW_CLASS);
  const meta = findWorkFieldMetaForInput(input);
  if (meta) meta.edt = corrected;
}

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

function setPageFillInputValue(input, value) {
  const next = String(value == null ? "" : value);
  const previous = String(input.value || "");
  if (previous === next) return false;
  beginPageFillHistorySession(input);
  markAutocorrectedInput(input, next);
  if (input.tagName === "SELECT" && !Array.from(input.options).some((opt) => opt.value === next)) {
    const opt = document.createElement("option");
    opt.value = next;
    opt.textContent = next;
    input.insertBefore(opt, input.firstChild);
  }
  input.value = next;
  input.dispatchEvent(new Event("input", { bubbles: true }));
  commitHistoryActionForInput(input);
  queueAutosaveForInput(input, false);
  return true;
}

function buildPageFillResultIndex(payload) {
  const byKey = new Map();
  const putFields = (kind, id, fields) => {
    if (id == null || !fields || typeof fields !== "object") return;
    Object.entries(fields).forEach(([fieldName, value]) => {
      byKey.set(`${kind}:${id}:${String(fieldName)}`, value);
    });
  };
  (Array.isArray(payload && payload.headers) ? payload.headers : []).forEach((row) => {
    if (!row || typeof row !== "object") return;
    putFields("h", row.header_id, row.fields);
  });
  (Array.isArray(payload && payload.records) ? payload.records : []).forEach((row) => {
    if (!row || typeof row !== "object") return;
    putFields("r", row.record_id, row.fields);
  });
  return byKey;
}

function buildPageFillReport(targets, resultIndex, ignored) {
  const corrected = [];
  const unchanged = [];
  const noMatch = [];
  targets.forEach((target) => {
    const id = target.record_id != null ? target.record_id : target.header_id;
    const kind = target.record_id != null ? "r" : "h";
    const key = `${kind}:${id}:${target.field_name}`;
    const displayName = formatFieldDisplayName(target.field_name, target);
    if (!resultIndex.has(key)) {
      noMatch.push({
        fieldName: target.field_name,
        recordId: target.record_id != null ? target.record_id : null,
        headerId: target.header_id != null ? target.header_id : null,
        displayName,
        hwr: "",
        location: formatFieldLocation(target),
        reason: "no-page-value",
      });
      return;
    }
    const output = String(resultIndex.get(key) == null ? "" : resultIndex.get(key));
    const before = String(target.input.value || "");
    if (before !== output) {
      corrected.push({
        fieldName: target.field_name,
        recordId: target.record_id != null ? target.record_id : null,
        headerId: target.header_id != null ? target.header_id : null,
        displayName,
        hwr: "",
        location: formatFieldLocation(target),
        before,
        after: output,
        reason: "matched-page-ai",
      });
    } else {
      unchanged.push({
        fieldName: target.field_name,
        recordId: target.record_id != null ? target.record_id : null,
        headerId: target.header_id != null ? target.header_id : null,
        displayName,
        hwr: "",
        location: formatFieldLocation(target),
        value: output,
        reason: "unchanged",
      });
    }
  });
  const ignoredCount = Array.isArray(ignored) ? ignored.length : 0;
  const summary =
    `${corrected.length} remplacé(s), ${unchanged.length} inchangé(s), ` +
    `${noMatch.length} sans valeur AI — ${targets.length} champ(s) visible(s)` +
    (ignoredCount ? `, ${ignoredCount} extra ignoré(s).` : ".");
  return { summary, corrected, unchanged, noMatch, total: targets.length };
}

function formatAiConfidence(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return "";
  const clamped = Math.max(0, Math.min(1, n));
  return (clamped * 100).toFixed(1).replace(/\.0$/, "") + "%";
}

function aiConfidenceLevel(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return "";
  if (n >= AI_CONF_HIGH_THRESHOLD) return "high";
  if (n < AI_CONF_LOW_THRESHOLD) return "low";
  return "medium";
}

function applyAiConfidenceBadge(input, confidence) {
  const text = formatAiConfidence(confidence);
  if (!input || !text) return;
  const level = aiConfidenceLevel(confidence);
  const parent = input.parentElement;
  if (!parent) return;
  const host = parent.classList.contains("field-table__orig-merged-input-wrap")
    ? parent
    : input.closest("td");
  if (!host) return;
  if (!host.classList.contains("field-table__orig-merged-input-wrap")) {
    host.classList.add("field-table__ai-conf-cell");
  }
  let badge = host.querySelector(".field-table__conf-badge--ai");
  if (!badge) {
    badge = document.createElement("span");
    badge.className = "field-table__conf-badge field-table__conf-badge--ai";
    host.appendChild(badge);
  }
  badge.classList.toggle("field-table__conf-badge--ai-high", level === "high");
  badge.classList.toggle("field-table__conf-badge--ai-medium", level === "medium");
  badge.classList.toggle("field-table__conf-badge--ai-low", level === "low");
  badge.textContent = text;
  badge.title = `AI confidence: ${text}. High >= 80%, low < 50%.`;
}

/**
 * Run dictionary auto-correction over the currently loaded work.
 * @param {{ source?: string, silent?: boolean }} options
 */
export async function applyDictionaryCorrections(options) {
  const opts = options || {};
  const silent = !!opts.silent;
  if (!state.batch || !state.work) {
    if (!silent) setStatus("Aucune image chargée pour la correction.", true);
    return { corrected: 0, skipped: 0, noMatch: 0 };
  }

  const targets = collectEligibleTargets();
  if (!targets.length) {
    if (!silent) setStatus("Aucun champ éligible à la correction par dictionnaire.");
    return { corrected: 0, skipped: 0, noMatch: 0 };
  }

  showDictAutocorrectLoading();
  try {
    let data;
    try {
      data = await api("/api/dictionary/autocorrect", {
        method: "POST",
        body: JSON.stringify({
          batch: String(state.batch),
          items: targets.map((t) => ({
            field_name: t.field_name,
            value: t.value,
            record_id: t.record_id,
            header_id: t.header_id,
          })),
        }),
      });
    } catch (e) {
      if (!silent) {
        setStatus(
          "Échec de la correction par dictionnaire: " + (e && e.message ? e.message : e),
          true
        );
      }
      return { corrected: 0, skipped: targets.length, noMatch: 0 };
    }

    const results = Array.isArray(data && data.results) ? data.results : [];
    const report = buildAutocorrectResultsReport(targets, results);

    results.forEach((res, idx) => {
      const target = targets[idx];
      if (!target) return;
      if (res && res.applied && res.output != null) {
        const output = String(res.output);
        if (String(target.input.value || "") !== output) {
          applyCorrectionToInput(target.input, output, target.value);
        }
      }
    });
    await flushAutosaveQueue("dictionary-autocorrect");

    const corrected = report.corrected.length;
    const noMatch = report.noMatch.length;
    const unchanged = report.unchanged.length;

    if (!silent) {
      setStatus(report.summary);
      await showDictAutocorrectResultsPanel(report);
    }

    return {
      corrected,
      unchanged,
      noMatch,
      skipped: unchanged,
      total: report.total,
      report,
    };
  } finally {
    hideDictAutocorrectLoading();
  }
}

export async function fetchAiAutocorrectStatus() {
  try {
    return await api("/api/ai/autocorrect/status");
  } catch (e) {
    return {
      ok: false,
      configured: false,
      reachable: false,
      available: false,
      error: e && e.message ? e.message : String(e || "AI indisponible"),
    };
  }
}

export async function applyAiCorrections(options) {
  const opts = options || {};
  const silent = !!opts.silent;
  if (!state.batch || !state.work) {
    if (!silent) setStatus("Aucune image chargée pour la correction.", true);
    return { corrected: 0, skipped: 0, noMatch: 0 };
  }

  const targets = collectEligibleAiTargets();
  if (!targets.length) {
    if (!silent) setStatus("Aucun champ éligible à la correction par AI.");
    return { corrected: 0, skipped: 0, noMatch: 0 };
  }

  const progress = createAutocorrectProgress("Correction par AI");
  const targetLabel =
    targets.length === 1 ? "1 champ" : `${targets.length} champs`;
  progressAddStep(progress, "prepare", `Préparation (${targetLabel})`);
  progressAddStep(progress, "ai", "Correction AI (passerelle Datafix)");
  progressAddStep(progress, "apply", "Application des résultats");
  progressAddStep(progress, "save", "Enregistrement");

  showDictAutocorrectLoading(null, progress);
  try {
    progressStartStep(progress, "prepare");
    progressCompleteStep(progress, "prepare");

    let data;
    const activeSubProject = getActiveSubProject();
    const language = activeSubProject ? String(activeSubProject.language || "").trim() : "";
    progressStartStep(progress, "ai");
    try {
      data = await api("/api/ai/autocorrect", {
        method: "POST",
        body: JSON.stringify({
          batch: String(state.batch),
          image_id: state.work.image_id,
          language,
          items: targets.map((t) => ({
            field_name: t.field_name,
            value: t.value,
            record_id: t.record_id,
            header_id: t.header_id,
            rect: t.rect,
          })),
        }),
      });
    } catch (e) {
      if (!silent) {
        setStatus(
          "Échec de la correction par AI: " + (e && e.message ? e.message : e),
          true
        );
      }
      return { corrected: 0, skipped: targets.length, noMatch: 0 };
    }
    progressCompleteStep(progress, "ai");

    progressStartStep(progress, "apply");
    const results = Array.isArray(data && data.results) ? data.results : [];
    const report = buildAutocorrectResultsReport(targets, results);

    results.forEach((res, idx) => {
      const target = targets[idx];
      if (!target) return;
      if (res && res.applied && res.output != null) {
        const output = String(res.output);
        if (String(target.input.value || "") !== output) {
          applyCorrectionToInput(target.input, output, target.value);
          applyAiConfidenceBadge(target.input, res.ai_confidence);
        }
      }
    });
    progressCompleteStep(progress, "apply");

    progressStartStep(progress, "save");
    await flushAutosaveQueue("ai-autocorrect");
    progressCompleteStep(progress, "save");
    report.aiStats = buildAiJobStats(data && data.ai_usage, performance.now() - progress.startedAt);

    const corrected = report.corrected.length;
    const noMatch = report.noMatch.length;
    const unchanged = report.unchanged.length;

    if (!silent) {
      setStatus(report.summary);
      await showDictAutocorrectResultsPanel(report);
    }

    return {
      corrected,
      unchanged,
      noMatch,
      skipped: unchanged,
      total: report.total,
      report,
    };
  } finally {
    hideDictAutocorrectLoading();
  }
}

export async function applyAiPageFill(options) {
  const opts = options || {};
  const silent = !!opts.silent;
  if (!state.batch || !state.work || state.work.image_id == null) {
    if (!silent) setStatus("Aucune image chargée pour AI Page Fill.", true);
    return { corrected: 0, skipped: 0, noMatch: 0 };
  }

  const targets = collectVisiblePageFillTargets();
  if (!targets.length) {
    if (!silent) setStatus("Aucun champ visible modifiable pour AI Page Fill.");
    return { corrected: 0, skipped: 0, noMatch: 0 };
  }
  const aiStatus = await fetchAiAutocorrectStatus();
  const pageFillMaxTargets = Math.max(
    1,
    Number(aiStatus && aiStatus.page_fill_max_fields) || AI_PAGE_FILL_MAX_TARGETS
  );
  if (targets.length > pageFillMaxTargets) {
    if (!silent) {
      setStatus(
        `AI Page Fill limité à ${pageFillMaxTargets} champs par lancement. ` +
          `${targets.length} champs visibles détectés : réduisez les champs visibles avant de relancer.`,
        true
      );
    }
    return { corrected: 0, skipped: targets.length, noMatch: 0 };
  }

  const progress = createAutocorrectProgress("Reconnaissance AI de toute la page");
  const targetLabel =
    targets.length === 1 ? "1 champ" : `${targets.length} champs`;
  progressAddStep(progress, "prepare", `Préparation (${targetLabel})`);
  progressAddStep(progress, "ai", "Reconnaissance AI (passerelle Datafix)");
  progressAddStep(progress, "apply", "Application des résultats");
  progressAddStep(progress, "save", "Enregistrement");

  showDictAutocorrectLoading(null, progress);
  try {
    progressStartStep(progress, "prepare");
    progressCompleteStep(progress, "prepare");

    let data;
    const activeSubProject = getActiveSubProject();
    const language = activeSubProject ? String(activeSubProject.language || "").trim() : "";
    const keyingInstructions =
      activeSubProject && Array.isArray(activeSubProject.keying_instructions)
        ? activeSubProject.keying_instructions
        : [];
    progressStartStep(progress, "ai");
    try {
      data = await api("/api/ai/page-fill", {
        method: "POST",
        body: JSON.stringify({
          batch: String(state.batch),
          image_id: state.work.image_id,
          language,
          doc_type: getActiveDocTypeForUi(),
          root_kind: getCurrentRootKind(),
          sub_project_name: activeSubProject ? String(activeSubProject.name || "") : "",
          sub_project: activeSubProject
            ? {
                name: String(activeSubProject.name || ""),
                root_kind: String(activeSubProject.root_kind || ""),
              }
            : {},
          keying_instructions: keyingInstructions,
          targets: targets.map((t) => ({
            field_name: t.field_name,
            field_label: t.field_label,
            record_id: t.record_id,
            header_id: t.header_id,
          })),
        }),
      });
    } catch (e) {
      if (!silent) {
        setStatus(
          "Échec de AI Page Fill: " + (e && e.message ? e.message : e),
          true
        );
      }
      return { corrected: 0, skipped: targets.length, noMatch: 0 };
    }
    progressCompleteStep(progress, "ai");

    progressStartStep(progress, "apply");
    const resultIndex = buildPageFillResultIndex(data);
    const report = buildPageFillReport(targets, resultIndex, data && data.ignored);

    targets.forEach((target) => {
      const id = target.record_id != null ? target.record_id : target.header_id;
      const kind = target.record_id != null ? "r" : "h";
      const key = `${kind}:${id}:${target.field_name}`;
      if (!resultIndex.has(key)) return;
      setPageFillInputValue(target.input, resultIndex.get(key));
    });
    progressCompleteStep(progress, "apply");

    progressStartStep(progress, "save");
    await flushAutosaveQueue("ai-page-fill");
    progressCompleteStep(progress, "save");
    report.aiStats = buildAiJobStats(data && data.ai_usage, performance.now() - progress.startedAt);

    if (!silent) {
      setStatus(report.summary);
      await showDictAutocorrectResultsPanel(report);
    }

    return {
      corrected: report.corrected.length,
      unchanged: report.unchanged.length,
      noMatch: report.noMatch.length,
      skipped: report.unchanged.length,
      total: report.total,
      report,
    };
  } finally {
    hideDictAutocorrectLoading();
  }
}
