// Pattern Matching 2D - frontend (UI semplificata operator-friendly)

// Parametri avanzati (sezione collassabile)
const ADV_PARAMS = [
  ["num_features", "Num feature", "int", ""],
  ["weak_grad", "Weak grad", "float", ""],
  ["strong_grad", "Strong grad", "float", ""],
  ["spread_radius", "Spread radius", "int", ""],
  ["pyramid_levels", "Pyramid levels", "int", ""],
  ["verify_threshold", "Verify NCC thr", "float", 0.4],
  ["nms_radius", "NMS radius (0=auto)", "int", 0],
];

const PALETTE = [
  "#00ff00", "#ffc800", "#ff6464", "#ffc800", "#c800ff",
  "#64ffc8", "#ff0000", "#00ffff",
];

const state = {
  model: null, scene: null, roi: null, drag: null,
  matches: [], annotatedImg: null,
  active_recipe: null,  // V: ricetta caricata (string nome) o null
};

// ---------- Forms ----------
function buildAdvancedForm() {
  const form = document.getElementById("adv-form");
  form.innerHTML = "";
  for (const [key, label, , def] of ADV_PARAMS) {
    const lbl = document.createElement("label");
    lbl.textContent = label;
    const inp = document.createElement("input");
    inp.id = `adv-${key}`;
    inp.type = "text";
    inp.placeholder = "auto";
    inp.value = def === "" ? "" : String(def);
    inp.addEventListener("keydown", (e) => {
      if (e.key === "Enter") doMatch();
    });
    form.appendChild(lbl);
    form.appendChild(inp);
  }
}

function readUserParams() {
  return {
    tipo: document.getElementById("p-tipo").value,
    simmetria: document.getElementById("p-simmetria").value,
    scala: document.getElementById("p-scala").value,
    precisione: document.getElementById("p-precisione").value,
    filtro_fp: document.getElementById("p-filtro-fp").value,
    penalita_scala: parseFloat(
      document.getElementById("p-penalita-scala").value),
    min_score: parseFloat(document.getElementById("p-min-score").value),
    max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
    ...readEdgeOverrides(),
    ...readHalconFlags(),
  };
}

function readEdgeOverrides() {
  // Override edge dal pannello "Anteprima edge". Settati = utente li ha
  // toccati (anche se uguali al default attuale). Vengono propagati a
  // _simple_to_technical e usati identici sia in training sia in find.
  // Inoltre salvati nella ricetta cosi' si replicano al load.
  const _v = (id, parser) => {
    const el = document.getElementById(id);
    if (!el) return null;
    const v = parser(el.value);
    return Number.isFinite(v) ? v : null;
  };
  // Sempre passa i valori correnti degli slider: e' la richiesta utente
  // che i param di pulizia rumore vengano usati anche nel find/ricetta.
  const polCb = document.getElementById("hc-use-polarity");
  return {
    edge_weak_grad: _v("ep-weak", parseFloat),
    edge_strong_grad: _v("ep-strong", parseFloat),
    edge_num_features: _v("ep-nf", parseInt),
    edge_min_feature_spacing: _v("ep-sp", parseInt),
    use_polarity: polCb?.checked || document.getElementById("ep-pol")?.checked,
  };
}

function readHalconFlags() {
  // Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
  const $cb = (id) => document.getElementById(id)?.checked ?? false;
  const $num = (id, def) => {
    const v = parseFloat(document.getElementById(id)?.value);
    return Number.isFinite(v) ? v : def;
  };
  const $int = (id, def) => {
    const v = parseInt(document.getElementById(id)?.value, 10);
    return Number.isFinite(v) ? v : def;
  };
  const roiStr = document.getElementById("hc-search-roi")?.value.trim() ?? "";
  let search_roi = null;
  if (roiStr) {
    const p = roiStr.split(/[ ,;]+/).map((x) => parseInt(x, 10));
    if (p.length === 4 && p.every((v) => Number.isFinite(v))) search_roi = p;
  }
  return {
    use_polarity: $cb("hc-use-polarity"),
    use_gpu: $cb("hc-use-gpu"),
    use_soft_score: $cb("hc-soft-score"),
    subpixel_lm: $cb("hc-subpixel-lm"),
    refine_pose_joint: $cb("hc-refine-joint"),
    pyramid_propagate: $cb("hc-pyr-propagate"),
    min_recall: $num("hc-min-recall", 0),
    nms_iou_threshold: $num("hc-nms-iou", 0.3),
    greediness: $num("hc-greediness", 0),
    coarse_stride: $int("hc-coarse-stride", 1),
    search_roi: search_roi,
  };
}

function readAdvancedOverrides() {
  const out = {};
  for (const [key, , type] of ADV_PARAMS) {
    const v = document.getElementById(`adv-${key}`).value.trim();
    if (v === "") continue;
    out[key] = type === "int" ? parseInt(v, 10) : parseFloat(v);
  }
  return out;
}

// ---------- Image loading from folder ----------
async function loadFromFolder(filename) {
  const r = await fetch("/load_from_folder", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ filename }),
  });
  if (!r.ok) throw new Error(await r.text());
  return await r.json();
}

async function fetchImagesList() {
  const r = await fetch("/images");
  if (!r.ok) return { files: [], dir: "" };
  return await r.json();
}

async function uploadToFolder(file) {
  const fd = new FormData();
  fd.append("file", file);
  const r = await fetch("/upload_to_folder", { method: "POST", body: fd });
  if (!r.ok) throw new Error(await r.text());
  return await r.json();
}

async function refreshPickers() {
  const {files, dir} = await fetchImagesList();
  buildThumbPicker("picker-model", files, onSelectModel);
  buildThumbPicker("picker-scene", files, onSelectScene);
  return {files, dir};
}

function buildThumbPicker(pickerId, files, onSelect) {
  const picker = document.getElementById(pickerId);
  const current = picker.querySelector(".picker-current");
  const list = picker.querySelector(".picker-list");
  const text = current.querySelector(".picker-text");
  // Rimuovi eventuale vecchia thumbnail
  const oldImg = current.querySelector("img");
  if (oldImg) oldImg.remove();
  list.innerHTML = "";

  files.forEach((f) => {
    const item = document.createElement("div");
    item.className = "picker-item";
    const img = document.createElement("img");
    img.src = `/folder_image/${encodeURIComponent(f)}?w=120`;
    img.loading = "lazy";
    const name = document.createElement("span");
    name.className = "name"; name.textContent = f;
    item.appendChild(img); item.appendChild(name);
    item.addEventListener("click", () => {
      // Aggiorna la visual del "current"
      let thumb = current.querySelector("img");
      if (!thumb) {
        thumb = document.createElement("img");
        current.insertBefore(thumb, text);
      }
      thumb.src = `/folder_image/${encodeURIComponent(f)}?w=80`;
      text.textContent = f;
      picker.classList.remove("open");
      onSelect(f);
    });
    list.appendChild(item);
  });

  current.onclick = () => {
    // Chiudi altri picker aperti
    document.querySelectorAll(".thumb-picker.open")
      .forEach((p) => { if (p !== picker) p.classList.remove("open"); });
    picker.classList.toggle("open");
  };
}

// Close picker on outside click
document.addEventListener("click", (e) => {
  if (!e.target.closest(".thumb-picker")) {
    document.querySelectorAll(".thumb-picker.open")
      .forEach((p) => p.classList.remove("open"));
  }
});

function loadImage(src) {
  return new Promise((res, rej) => {
    const img = new Image();
    img.onload = () => res(img);
    img.onerror = rej;
    img.src = src;
  });
}

async function onSelectModel(filename) {
  if (!filename) return;
  setStatus("Caricamento modello...");
  try {
    const meta = await loadFromFolder(filename);
    const img = await loadImage(`/image/${meta.id}/raw`);
    state.model = { id: meta.id, w: meta.width, h: meta.height, img };
    state.roi = null;
    document.getElementById("roi-info").textContent = "ROI: (nessuna)";
    setStatus(`Modello: ${filename} ${meta.width}x${meta.height} — trascina ROI`);
    renderModel();
  } catch (e) {
    setStatus(`Errore modello: ${e.message}`);
  }
}

async function onSelectScene(filename) {
  if (!filename) return;
  setStatus("Caricamento scena...");
  try {
    const meta = await loadFromFolder(filename);
    const img = await loadImage(`/image/${meta.id}/raw`);
    state.scene = { id: meta.id, w: meta.width, h: meta.height, img };
    state.matches = []; state.annotatedImg = null;
    setStatus(`Scena: ${filename} ${meta.width}x${meta.height}`);
    renderScene();
  } catch (e) {
    setStatus(`Errore scena: ${e.message}`);
  }
}

// ---------- Rendering ----------
function fitToCanvas(img, cw, ch) {
  const sc = Math.min(cw / img.width, ch / img.height);
  const dw = img.width * sc, dh = img.height * sc;
  return { sc, ox: (cw - dw) / 2, oy: (ch - dh) / 2, dw, dh };
}

function renderModel() {
  const cnv = document.getElementById("c-model");
  const ctx = cnv.getContext("2d");
  ctx.fillStyle = "#141414";
  ctx.fillRect(0, 0, cnv.width, cnv.height);
  if (!state.model) return;
  const fit = fitToCanvas(state.model.img, cnv.width, cnv.height);
  state.model.scale = fit.sc;
  state.model.ox = fit.ox; state.model.oy = fit.oy;
  ctx.drawImage(state.model.img, fit.ox, fit.oy, fit.dw, fit.dh);
  if (state.roi) {
    const [x, y, w, h] = state.roi;
    ctx.strokeStyle = "#00ff80"; ctx.lineWidth = 2;
    ctx.strokeRect(fit.ox + x * fit.sc, fit.oy + y * fit.sc,
                   w * fit.sc, h * fit.sc);
  }
  if (state.drag) {
    ctx.strokeStyle = "#ffff00";
    ctx.setLineDash([4, 2]); ctx.lineWidth = 2;
    ctx.strokeRect(
      Math.min(state.drag.x0, state.drag.x1),
      Math.min(state.drag.y0, state.drag.y1),
      Math.abs(state.drag.x1 - state.drag.x0),
      Math.abs(state.drag.y1 - state.drag.y0));
    ctx.setLineDash([]);
  }
}

function renderScene() {
  const cnv = document.getElementById("c-scene");
  const ctx = cnv.getContext("2d");
  ctx.fillStyle = "#141414";
  ctx.fillRect(0, 0, cnv.width, cnv.height);
  const img = state.annotatedImg || (state.scene && state.scene.img);
  if (!img) return;
  const fit = fitToCanvas(img, cnv.width, cnv.height);
  ctx.drawImage(img, fit.ox, fit.oy, fit.dw, fit.dh);
}

// ---------- ROI drag ----------
function canvasPos(cnv, ev) {
  const r = cnv.getBoundingClientRect();
  return { x: ev.clientX - r.left, y: ev.clientY - r.top };
}

function setupROI() {
  const cnv = document.getElementById("c-model");
  cnv.addEventListener("mousedown", (e) => {
    if (!state.model) return;
    const p = canvasPos(cnv, e);
    state.drag = { x0: p.x, y0: p.y, x1: p.x, y1: p.y };
    renderModel();
  });
  cnv.addEventListener("mousemove", (e) => {
    if (!state.drag) return;
    const p = canvasPos(cnv, e);
    state.drag.x1 = p.x; state.drag.y1 = p.y;
    renderModel();
  });
  cnv.addEventListener("mouseup", () => {
    if (!state.drag || !state.model) return;
    const d = state.drag; state.drag = null;
    if (Math.abs(d.x1 - d.x0) < 4 || Math.abs(d.y1 - d.y0) < 4) return;
    const m = state.model;
    const ix0 = Math.round((Math.min(d.x0, d.x1) - m.ox) / m.scale);
    const iy0 = Math.round((Math.min(d.y0, d.y1) - m.oy) / m.scale);
    const iw = Math.round(Math.abs(d.x1 - d.x0) / m.scale);
    const ih = Math.round(Math.abs(d.y1 - d.y0) / m.scale);
    const cx0 = Math.max(0, Math.min(ix0, m.w - 1));
    const cy0 = Math.max(0, Math.min(iy0, m.h - 1));
    const cw = Math.max(1, Math.min(iw, m.w - cx0));
    const ch = Math.max(1, Math.min(ih, m.h - cy0));
    state.roi = [cx0, cy0, cw, ch];
    document.getElementById("roi-info").textContent =
      `ROI: ${cw}x${ch} @ (${cx0}, ${cy0})`;
    renderModel();
  });
}

// ---------- Match action ----------
async function doMatchRecipe() {
  if (!state.scene) { setStatus("Carica scena"); return; }
  setStatus(`Match ricetta ${state.active_recipe}...`);
  const hc = readHalconFlags();
  const body = {
    recipe: state.active_recipe,
    scene_id: state.scene.id,
    min_score: parseFloat(document.getElementById("p-min-score").value),
    max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
    verify_threshold: 0.50,
    ...hc,
  };
  const r = await fetch("/match_recipe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!r.ok) { setStatus(`Errore: ${await r.text()}`); return; }
  const data = await r.json();
  state.matches = data.matches;
  state.annotatedImg = await loadImage(
    `/image/${data.annotated_id}/raw?t=${Date.now()}`);
  renderScene();
  renderLegend();
  document.getElementById("t-train").textContent = "—";
  document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
  document.getElementById("t-var").textContent = data.num_variants;
  document.getElementById("t-match").textContent = data.matches.length;
  renderDiag(data.diag, data.matches.length);
  setStatus(`${data.matches.length} match trovati (ricetta ${state.active_recipe})`);
}

async function doMatch() {
  // Path V: ricetta caricata → bypass training, solo find su scena
  if (state.active_recipe) {
    return doMatchRecipe();
  }
  if (!state.model) { setStatus("Carica modello"); return; }
  if (!state.scene) { setStatus("Carica scena"); return; }
  if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; }
  const user = readUserParams();
  const adv = readAdvancedOverrides();
  setStatus("Match in corso...");

  // Se utente ha fornito override avanzati → usa /match (tecnico)
  // altrimenti /match_simple (operator mode)
  const hasAdv = Object.keys(adv).length > 0;
  const url = hasAdv ? "/match" : "/match_simple";
  let body;
  if (hasAdv) {
    // Merge simple → tecnici base, poi overrides
    const SYM_MAP = {invariante:0, nessuna:360, bilaterale:180, rot_3:120,
                     rot_4:90, rot_6:60, rot_8:45};
    const SCALE_MAP = {fissa:[1,1,0.1], mini:[0.9,1.1,0.05],
                       medio:[0.75,1.25,0.05], max:[0.5,1.5,0.05]};
    const PREC_MAP = {veloce:10, normale:5, preciso:2};
    // Allineato a FILTRO_FP_MAP server-side (server.py)
    const FP_MAP = {off:0, leggero:0.30, medio:0.50, forte:0.70};
    const [smin, smax, sstep] = SCALE_MAP[user.scala];
    // NB: SYM_MAP[invariante]=0 e' valido (zero rotazioni). Uso ?? per
    // distinguere "chiave mancante" da "valore zero": altrimenti 0 || 360
    // collassa invariante a 360 = bug "simmetria non ha effetto".
    const angMax = SYM_MAP[user.simmetria] ?? 360;
    body = {
      model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
      angle_min: 0, angle_max: angMax,
      angle_step: PREC_MAP[user.precisione] ?? 5,
      scale_min: smin, scale_max: smax, scale_step: sstep,
      min_score: user.min_score, max_matches: user.max_matches,
      num_features: adv.num_features ?? 96,
      weak_grad: adv.weak_grad ?? 30,
      strong_grad: adv.strong_grad ?? 60,
      spread_radius: adv.spread_radius ?? 5,
      pyramid_levels: adv.pyramid_levels ?? 3,
      verify_threshold: adv.verify_threshold ?? (FP_MAP[user.filtro_fp] ?? 0.50),
      nms_radius: adv.nms_radius ?? 0,
    };
  } else {
    body = {
      model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
      ...user,
    };
  }

  const r = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!r.ok) {
    setStatus(`Errore: ${await r.text()}`); return;
  }
  const data = await r.json();
  state.matches = data.matches;
  state.annotatedImg = await loadImage(
    `/image/${data.annotated_id}/raw?t=${Date.now()}`);
  renderScene();
  renderLegend();
  document.getElementById("t-train").textContent = `${data.train_time.toFixed(2)}s`;
  document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
  document.getElementById("t-var").textContent = data.num_variants;
  document.getElementById("t-match").textContent = data.matches.length;
  renderDiag(data.diag, data.matches.length);
  setStatus(`${data.matches.length} match trovati${hasAdv ? " (avanzato)" : ""}`);
}

function renderLegend() {
  const el = document.getElementById("legend");
  el.innerHTML = "";
  state.matches.forEach((m, i) => {
    const div = document.createElement("div");
    div.className = "legend-item";
    const dot = document.createElement("span");
    dot.className = "legend-dot";
    dot.style.background = PALETTE[i % PALETTE.length];
    div.appendChild(dot);
    const txt = document.createElement("span");
    txt.textContent = `#${i+1}  cx=${Math.round(m.cx)} cy=${Math.round(m.cy)}  `
      + `${m.angle_deg.toFixed(1)}°  s=${m.scale.toFixed(2)}  `
      + `score=${m.score.toFixed(3)}`;
    div.appendChild(txt);
    el.appendChild(div);
  });
}

function setStatus(s) {
  document.getElementById("status").textContent = s;
}

// ---------- Init ----------
// ---------- Edge preview (clean rumore) ----------
let _epDebounce = null;
let _epLastImg = null;

async function fetchEdgePreview() {
  if (!state.model || !state.roi) {
    document.getElementById("edge-preview-info").textContent =
      "Disegna prima la ROI sul modello";
    return;
  }
  const body = {
    model_id: state.model.id,
    roi: state.roi,
    weak_grad: parseFloat(document.getElementById("ep-weak").value),
    strong_grad: parseFloat(document.getElementById("ep-strong").value),
    num_features: parseInt(document.getElementById("ep-nf").value, 10),
    min_feature_spacing: parseInt(document.getElementById("ep-sp").value, 10),
    use_polarity: document.getElementById("ep-pol").checked,
  };
  try {
    const r = await fetch("/preview_edges", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(await r.text());
    const j = await r.json();
    _epLastImg = await loadImage(`/image/${j.preview_id}/raw?t=${Date.now()}`);
    drawEdgePreview();
    const ucs = j.ucs_baricentro
      ? ` | UCS=(${j.ucs_baricentro.cx},${j.ucs_baricentro.cy})`
      : "";
    document.getElementById("edge-preview-info").innerHTML =
      `<b>${j.n_features}</b> feature scelte (di ${j.n_edge_after_hysteresis} edge totali)<br>` +
      `mag: max=${j.mag_max.toFixed(0)} p50=${j.mag_p50.toFixed(0)} ` +
      `p85=${j.mag_p85.toFixed(0)}${ucs}`;
  } catch (e) {
    document.getElementById("edge-preview-info").textContent =
      `Errore preview: ${e.message}`;
  }
}

function drawEdgePreview() {
  const cnv = document.getElementById("c-edge-preview");
  if (!_epLastImg) return;
  const ctx = cnv.getContext("2d");
  // Fit-contain
  const r = Math.min(cnv.width / _epLastImg.width,
                     cnv.height / _epLastImg.height);
  const w = _epLastImg.width * r;
  const h = _epLastImg.height * r;
  const ox = (cnv.width - w) / 2;
  const oy = (cnv.height - h) / 2;
  ctx.fillStyle = "#000"; ctx.fillRect(0, 0, cnv.width, cnv.height);
  ctx.imageSmoothingEnabled = false;
  ctx.drawImage(_epLastImg, ox, oy, w, h);
}

function scheduleEdgePreview() {
  if (_epDebounce) clearTimeout(_epDebounce);
  _epDebounce = setTimeout(fetchEdgePreview, 200);
}

function bindEdgePreviewControls() {
  const slid = (id, valEl) => {
    const el = document.getElementById(id);
    const v = document.getElementById(valEl);
    el.addEventListener("input", () => {
      v.textContent = el.value;
      scheduleEdgePreview();
    });
  };
  slid("ep-weak", "ep-weak-v");
  slid("ep-strong", "ep-strong-v");
  slid("ep-nf", "ep-nf-v");
  slid("ep-sp", "ep-sp-v");
  document.getElementById("ep-pol").addEventListener("change",
    scheduleEdgePreview);
  // Auto-refresh quando il pannello viene aperto
  document.getElementById("edge-preview-panel").addEventListener("toggle",
    (e) => { if (e.target.open) fetchEdgePreview(); });
  document.getElementById("btn-edge-apply").addEventListener("click", () => {
    // Copia i valori correnti nei campi avanzati
    const map = {
      "ep-weak": "adv-weak_grad",
      "ep-strong": "adv-strong_grad",
      "ep-nf": "adv-num_features",
      "ep-sp": "adv-min_feature_spacing",
    };
    for (const [src, dst] of Object.entries(map)) {
      const dstEl = document.getElementById(dst);
      if (dstEl) dstEl.value = document.getElementById(src).value;
    }
    // use_polarity: alla checkbox della modalita Halcon
    const polCb = document.getElementById("hc-use-polarity");
    if (polCb) polCb.checked = document.getElementById("ep-pol").checked;
    // Apri pannello Avanzate per feedback
    const advDetails = document.querySelectorAll("#col-params details");
    advDetails.forEach((d) => { d.open = true; });
    alert("Parametri edge applicati. Esegui MATCH per usare i valori scelti.");
  });
}

// ---------- CC: Diagnostica match ----------
function renderDiag(diag, n_matches) {
  const el = document.getElementById("diag-content");
  if (!diag) {
    el.innerHTML = '<em style="color:#888">Diagnostica non disponibile</em>';
    return;
  }
  const dropTotal = (diag.drop_ncc_low || 0) + (diag.drop_min_score_post_avg || 0)
    + (diag.drop_recall_low || 0) + (diag.drop_bbox_out_of_scene || 0)
    + (diag.drop_nms_iou || 0);
  // Hint contestuali se 0 match
  let hint = "";
  if (n_matches === 0) {
    if (diag.n_after_pre_nms === 0) {
      hint = `<div style="color:#f88; margin-top:6px">⚠ Nessun candidato sopra soglia.
        Prova: ↓ <b>min_score</b> o ↓ <b>top_thresh</b> (currently ${diag.top_thresh_used.toFixed(2)})</div>`;
    } else if (diag.drop_ncc_low > 0 && dropTotal === diag.drop_ncc_low) {
      hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_ncc_low} candidati droppati da NCC.
        Prova: ↓ <b>verify_threshold</b> (filtro_fp più leggero)</div>`;
    } else if (diag.drop_min_score_post_avg > 0) {
      hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_min_score_post_avg} match sotto min_score post-NCC.
        Prova: ↓ <b>min_score</b></div>`;
    } else if (diag.drop_recall_low > 0) {
      hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_recall_low} match con recall < ${diag.min_recall_used}.
        Prova: ↓ <b>min_recall</b></div>`;
    } else if (diag.drop_bbox_out_of_scene > 0) {
      hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_bbox_out_of_scene} match con bbox fuori scena.
        Centro derivato male: aumenta <b>min_score</b> o restringi <b>search_roi</b></div>`;
    }
  }
  const flags = [];
  if (diag.use_polarity) flags.push("polarity");
  if (diag.use_soft_score) flags.push("soft");
  if (diag.subpixel_lm) flags.push("subpix-LM");
  el.innerHTML = `
    <div><b>Pipeline pruning:</b></div>
    <div>varianti: ${diag.n_variants_total} → top_eval=${diag.n_variants_top_evaluated}
       → top_pass=${diag.n_variants_top_passed} → full_eval=${diag.n_variants_full_evaluated}</div>
    <div><b>Candidati:</b> raw=${diag.n_raw_candidates}
       → pre_nms=${diag.n_after_pre_nms} → final=${diag.n_final}</div>
    <div><b>Drop reasons:</b> NCC=${diag.drop_ncc_low}, score=${diag.drop_min_score_post_avg},
       recall=${diag.drop_recall_low}, bbox=${diag.drop_bbox_out_of_scene}, NMS=${diag.drop_nms_iou}</div>
    <div><b>Soglie:</b> top=${diag.top_thresh_used.toFixed(2)},
       min_score=${diag.min_score_used.toFixed(2)},
       NCC=${diag.verify_threshold_used.toFixed(2)},
       recall=${diag.min_recall_used.toFixed(2)}</div>
    ${flags.length ? `<div><b>Flag attivi:</b> ${flags.join(", ")}</div>` : ""}
    ${hint}
  `;
  // Auto-apri pannello se 0 match (segnala problema)
  if (n_matches === 0) {
    document.getElementById("diag-panel").open = true;
  }
}

// ---------- Auto-tune (Halcon-style) ----------
async function doAutoTune() {
  if (!state.model || !state.roi) {
    alert("Seleziona modello e disegna ROI prima di Auto-tune.");
    return;
  }
  const status = document.getElementById("status");
  status.textContent = "Analisi ROI in corso...";
  try {
    const r = await fetch("/auto_tune", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model_id: state.model.id,
        roi: state.roi,
      }),
    });
    if (!r.ok) throw new Error(await r.text());
    const t = await r.json();
    // Applica ai campi avanzati (override automatico)
    for (const [key] of ADV_PARAMS) {
      const el = document.getElementById(`adv-${key}`);
      if (el && t[key] !== undefined) el.value = String(t[key]);
    }
    // Espandi la sezione Avanzate per mostrare i valori applicati
    const advDetails = document.querySelector("#col-params details:last-of-type");
    if (advDetails) advDetails.open = true;
    // Feedback diagnostico
    const lines = [
      `weak/strong: ${t.weak_grad} / ${t.strong_grad}`,
      `feature: ${t.num_features}, piramide: ${t.pyramid_levels}`,
      `angle: [${t.angle_min}..${t.angle_max}]@${t.angle_step}°`,
    ];
    if (t._symmetry_order > 1) {
      lines.push(`simmetria rotaz. ${t._symmetry_order}x (conf ${t._symmetry_conf})`);
    }
    if (t._self_score !== undefined) {
      lines.push(`self-validation: ${t._validation}`);
    }
    status.textContent = `Auto-tune OK — ${lines[0]}`;
    alert("Auto-tune completato:\n\n" + lines.join("\n"));
  } catch (e) {
    status.textContent = `Auto-tune errore: ${e.message}`;
    alert(`Errore auto-tune: ${e.message}`);
  }
}

// ---------- V: Recipe load/list/unload ----------
async function refreshRecipeList() {
  try {
    const r = await fetch("/recipes");
    if (!r.ok) return;
    const j = await r.json();
    const sel = document.getElementById("hc-recipe-list");
    const cur = sel.value;
    sel.innerHTML = '<option value="">— ricette disponibili —</option>';
    for (const f of j.files) {
      const o = document.createElement("option");
      o.value = f.name;
      o.textContent = `${f.name}  (${(f.size / 1024).toFixed(1)} KB)`;
      sel.appendChild(o);
    }
    if (cur) sel.value = cur;
  } catch (e) { /* silent */ }
}

async function loadRecipe() {
  const sel = document.getElementById("hc-recipe-list");
  const name = sel.value;
  if (!name) {
    alert("Seleziona una ricetta dalla lista.");
    return;
  }
  try {
    const r = await fetch(`/recipes/${encodeURIComponent(name)}/load`, {
      method: "POST",
    });
    if (!r.ok) throw new Error(await r.text());
    const j = await r.json();
    state.active_recipe = j.name;
    document.getElementById("recipe-status").textContent =
      `Caricata: ${j.name} — ${j.n_variants} varianti, ` +
      `${j.template_size[0]}x${j.template_size[1]} px` +
      (j.use_polarity ? " (polarity)" : "");
    document.getElementById("recipe-status").style.color = "#0c0";
    document.getElementById("btn-unload-recipe").disabled = false;
  } catch (e) {
    alert(`Errore caricamento: ${e.message}`);
  }
}

function unloadRecipe() {
  state.active_recipe = null;
  document.getElementById("recipe-status").textContent = "Nessuna ricetta caricata";
  document.getElementById("recipe-status").style.color = "#888";
  document.getElementById("btn-unload-recipe").disabled = true;
}

// ---------- V: Save recipe ----------
async function saveRecipe() {
  if (!state.model || !state.roi) {
    alert("Seleziona modello e disegna ROI prima di salvare la ricetta.");
    return;
  }
  const name = document.getElementById("hc-recipe-name").value.trim();
  if (!name) {
    alert("Inserisci un nome per la ricetta.");
    return;
  }
  const user = readUserParams();
  const body = {
    model_id: state.model.id,
    scene_id: state.scene?.id || state.model.id,
    roi: state.roi,
    tipo: user.tipo,
    simmetria: user.simmetria,
    scala: user.scala,
    precisione: user.precisione,
    use_polarity: user.use_polarity,
    use_gpu: user.use_gpu,
    edge_weak_grad: user.edge_weak_grad,
    edge_strong_grad: user.edge_strong_grad,
    edge_num_features: user.edge_num_features,
    edge_min_feature_spacing: user.edge_min_feature_spacing,
    name: name,
  };
  try {
    const r = await fetch("/recipes", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(await r.text());
    const j = await r.json();
    alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`);
    refreshRecipeList();
  } catch (e) {
    alert(`Errore salvataggio: ${e.message}`);
  }
}

window.addEventListener("DOMContentLoaded", async () => {
  buildAdvancedForm();
  setupROI();
  // Popola picker immagini da IMAGES_DIR (con thumbnail)
  const {files, dir} = await refreshPickers();
  if (files.length === 0) {
    setStatus(`Nessuna immagine in ${dir} (carica file o configura IMAGES_DIR)`);
  } else {
    setStatus(`${files.length} immagini in ${dir}`);
  }

  // Upload file nella folder
  const upEl = document.getElementById("file-upload");
  upEl.addEventListener("change", async (e) => {
    const f = e.target.files[0];
    if (!f) return;
    setStatus(`Caricamento ${f.name} nella cartella...`);
    try {
      const res = await uploadToFolder(f);
      await refreshPickers();
      setStatus(`Salvato come ${res.saved_as} (${res.files.length} file totali)`);
    } catch (err) {
      setStatus(`Errore upload: ${err.message}`);
    }
    e.target.value = "";  // consente re-upload stesso file
  });
  document.getElementById("btn-match").addEventListener("click", doMatch);
  document.getElementById("btn-autotune").addEventListener("click", doAutoTune);
  document.getElementById("btn-save-recipe").addEventListener("click",
    saveRecipe);
  document.getElementById("btn-load-recipe").addEventListener("click",
    loadRecipe);
  document.getElementById("btn-unload-recipe").addEventListener("click",
    unloadRecipe);
  refreshRecipeList();
  bindEdgePreviewControls();
  const slider = document.getElementById("p-min-score");
  slider.addEventListener("input", (e) => {
    document.getElementById("v-score").textContent =
      parseFloat(e.target.value).toFixed(2);
  });
  renderModel();
  renderScene();
});
