'use strict';

// ── PDF.js worker ────────────────────────────────────────────────
if (typeof pdfjsLib !== 'undefined') {
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}

// ── Diary config ─────────────────────────────────────────────────
const DIARY_CONFIG = {
  'calgary-highlanders': {
    pdfPath: '../Inputs/Calgary-Highlanders_War-Diary_Sep44.pdf',
    textSources: [
      { label: 'olmOCR — viewer',    path: '../outputs_step2_json_to_viewer/Calgary-Highlanders_War-Diary_Sep44_olmocr_viewer.md' },
      { label: 'Claude 4.6 — viewer', path: '../outputs_step2_json_to_viewer/step2_Calgary-Highlanders_War-Diary_Sep44_claude-4-6_viewer.md' },
    ],
  },
  'black-watch': {
    pdfPath: '../Inputs/RHC-Blackwatch_War-Diary_Sep44.pdf',
    textSources: [
      { label: 'olmOCR', path: '../Inputs/ocr-output/RHC-Blackwatch_War-Diary_Sep44_olmocr.md' },
    ],
  },
  '5cib': {
    pdfPath: '../Inputs/5CIB_War-Diary_Sep44.pdf',
    textSources: [
      { label: 'olmOCR', path: '../Inputs/ocr-output/5CIB_War-Diary_Sep44_olmocr.md' },
    ],
  },
};

const diaryId = new URLSearchParams(window.location.search).get('diary') || 'calgary-highlanders';
const _diary  = DIARY_CONFIG[diaryId] || DIARY_CONFIG['calgary-highlanders'];

// ── Data paths (served by launch_viewer.py) ───────────────────────
const REAL_PDF_PATH    = _diary.pdfPath;
let   REAL_TEXT_PATH   = _diary.textSources[0].path;

// ── Per-diary storage keys / server endpoints ─────────────────────
const LS_FLAGS_KEY       = 'p44-flags-'       + diaryId;
const LS_PASS2_FLAGS_KEY = 'p44-pass2-flags-' + diaryId;
const LS_CORRECTIONS_KEY = 'p44-corrections-' + diaryId;

const SERVER_FLAGS_FILE       = 'viewer/data/flags-'       + diaryId + '.json';
const SERVER_PASS2_FLAGS_FILE = 'viewer/data/pass2_flags-' + diaryId + '.json';
const SERVER_CORRECTIONS_FILE = 'viewer/data/corrections-' + diaryId + '.json';
const SERVER_FLAGS_ENDPOINT   = '/save-flags?diary='        + diaryId;
const SERVER_PASS2_ENDPOINT   = '/save-pass2-flags?diary='  + diaryId;
const SERVER_CORRECTIONS_END  = '/save-corrections?diary='  + diaryId;
const SERVER_CLEAR_ENDPOINT   = '/clear-all-data?diary='    + diaryId;

// ── Diary display names ───────────────────────────────────────────
const DIARY_TITLES = {
  'calgary-highlanders': 'Calgary Highlanders — Sep 1944',
  'black-watch':         'Black Watch (RHR) — Sep 1944',
  '5cib':                '5th Canadian Infantry Brigade — Sep 1944',
};

// ─────────────────────────────────────────────────────────────────
// STATE
// ─────────────────────────────────────────────────────────────────
const state = {
  mode: 'none',       // 'pdf' | 'none'
  pdfDoc: null,
  currentPage: 1,
  totalPages: 0,
  zoomScale: 1.0,
  wordData: [],
  rendering: false,
  ocrPages: {},
  lastRenderTime: 0,
  flags: {},
  corrections: {},
  ocrOverlay: false,
};

// ─────────────────────────────────────────────────────────────────
// DOM REFS
// ─────────────────────────────────────────────────────────────────
const inputPdf    = document.getElementById('input-pdf');
const inputText   = document.getElementById('input-text');
const inputJson   = document.getElementById('input-json');

const imagePlaceholder = document.getElementById('image-placeholder');
const imgSpinner       = document.getElementById('img-spinner');
const imgPhText        = document.getElementById('img-ph-text');
const pdfContainer     = document.getElementById('pdf-container');
const pdfCanvas        = document.getElementById('pdf-canvas');
const overlay          = document.getElementById('overlay');

const textPlaceholder  = document.getElementById('text-placeholder');
const txtSpinner       = document.getElementById('txt-spinner');
const txtPhText        = document.getElementById('txt-ph-text');

const btnPrev      = document.getElementById('btn-prev');
const btnNext      = document.getElementById('btn-next');
const pageInput    = document.getElementById('page-input');
const pageLabel    = document.getElementById('page-label');
const ocrSyncLabel = document.getElementById('ocr-sync-label');
const statusMsg    = document.getElementById('status-msg');

// Phase 2 — panel body refs (used for edit mode layout)
const imagePanelBody = document.getElementById('image-panel-body');
const textPanelBody  = document.getElementById('text-panel-body');

// Zoom indicator
const zoomLabel    = document.getElementById('zoom-label');
const btnZoomReset = document.getElementById('btn-zoom-reset');

// Phase 4 — flags
const btnFlagDone    = document.getElementById('btn-flag-done');
const btnFlagProblem = document.getElementById('btn-flag-problem');
const flagsSummary    = document.getElementById('flags-summary');

// Phase 5 — edit surface
const editorWrap      = document.getElementById('editor-wrap');
const lineNumbers     = document.getElementById('line-numbers');
const textEditor      = document.getElementById('text-editor');

// Phase 6 — OCR overlay toggle (Pass 2)
const btnOcrToggle    = document.getElementById('btn-ocr-toggle');
const ocrOverlayPanel = document.getElementById('ocr-overlay-panel');

// ─────────────────────────────────────────────────────────────────
// OVERRIDE DROPDOWN
// ─────────────────────────────────────────────────────────────────
const btnOverride    = document.getElementById('btn-override');
const overrideMenu   = document.getElementById('override-menu');

btnOverride.addEventListener('click', e => {
  e.stopPropagation();
  overrideMenu.classList.toggle('open');
});

// Close when a file input is activated (user picked a file or cancelled)
document.getElementById('input-pdf') .addEventListener('change', () => overrideMenu.classList.remove('open'));
document.getElementById('input-text').addEventListener('change', () => overrideMenu.classList.remove('open'));
document.getElementById('input-json').addEventListener('change', () => overrideMenu.classList.remove('open'));

// Close when clicking anywhere outside the dropdown
document.addEventListener('click', () => overrideMenu.classList.remove('open'));

// ─────────────────────────────────────────────────────────────────
// TOOLBAR BUTTON EVENTS
// ─────────────────────────────────────────────────────────────────
inputPdf.addEventListener('change', async e => {
  const file = e.target.files[0]; if (!file) return;
  setStatus('Loading PDF: ' + file.name + '…'); showImgSpinner(true);
  const buf = await file.arrayBuffer();
  showImgSpinner(false);
  await loadPDFBuffer(buf);
  setStatus('PDF: ' + file.name + ' — ' + state.totalPages + ' pages');
});

inputText.addEventListener('change', e => {
  const file = e.target.files[0]; if (!file) return;
  const reader = new FileReader();
  reader.onload = ev => { loadOCRText(ev.target.result, true); setStatus('Text: ' + file.name); };
  reader.readAsText(file);
});

inputJson.addEventListener('change', e => {
  const file = e.target.files[0]; if (!file) return;
  const reader = new FileReader();
  reader.onload = ev => {
    try {
      loadWordDataJSON(JSON.parse(ev.target.result));
      setStatus('JSON: ' + file.name + ' — ' + state.wordData.length + ' words');
    } catch (err) { alert('JSON parse error: ' + err.message); }
  };
  reader.readAsText(file);
});

btnPrev.addEventListener('click', () => {
  if (currentRole === 'pass2') {
    const verified = getVerifiedPages();
    if (verified.length) {
      const prev = [...verified].reverse().find(p => p < state.currentPage);
      if (prev !== undefined) goToPage(prev);
    } else {
      goToPage(state.currentPage - 1);  // no queue yet — sequential fallback
    }
  } else {
    goToPage(state.currentPage - 1);
  }
});
btnNext.addEventListener('click', () => {
  if (currentRole === 'pass2') {
    const verified = getVerifiedPages();
    if (verified.length) {
      const next = verified.find(p => p > state.currentPage);
      if (next !== undefined) goToPage(next);
    } else {
      goToPage(state.currentPage + 1);  // no queue yet — sequential fallback
    }
  } else {
    goToPage(state.currentPage + 1);
  }
});
pageInput.addEventListener('change', () => {
  const n = parseInt(pageInput.value, 10);
  if (!isNaN(n)) goToPage(n);
});

window.addEventListener('resize', () => {
  if (state.mode === 'pdf' && state.pdfDoc && !state.rendering) renderCurrentPage();
  updateLineNumbers();
});

// ── Auto-load on startup when served from localhost ───────────────
window.addEventListener('DOMContentLoaded', () => {
  // Set diary title in toolbar and browser tab
  const diaryTitle = DIARY_TITLES[diaryId] || diaryId;
  document.title = 'P44 OCR Viewer — ' + diaryTitle;
  const diaryLabel = document.getElementById('diary-label');
  if (diaryLabel) diaryLabel.textContent = diaryTitle;

  // Populate OCR source dropdown
  const sourceSelect = document.getElementById('ocr-source-select');
  if (sourceSelect && _diary.textSources) {
    _diary.textSources.forEach((src, i) => {
      const opt = document.createElement('option');
      opt.value = src.path;
      opt.textContent = src.label;
      sourceSelect.appendChild(opt);
    });
    // Ensure the dropdown reflects the actual initially-loaded source, overriding
    // any browser-restored form state that could cause a visual mismatch.
    sourceSelect.value = REAL_TEXT_PATH;
    sourceSelect.addEventListener('change', async () => {
      REAL_TEXT_PATH = sourceSelect.value;
      txtSpinner.style.display = 'inline-block';
      try {
        const r = await fetch(REAL_TEXT_PATH + '?v=' + Date.now());
        if (!r.ok) throw new Error('HTTP ' + r.status);
        const text = await r.text();
        // Clear stale per-page "original" snapshots so the new source content
        // is displayed instead of the previously-cached text.
        Object.values(state.corrections).forEach(c => { delete c.original; });
        loadOCRText(text, true);
        if (state.pdfDoc) showOCRPage(state.currentPage || 1);
        setStatus('OCR source changed — ' + sourceSelect.options[sourceSelect.selectedIndex].text);
      } catch (err) {
        showTxtPlaceholder('Could not load OCR source: ' + err.message);
      } finally {
        txtSpinner.style.display = 'none';
      }
    });
  }

  if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
    setTimeout(loadWarDiary, 200);
  }
});

// ─────────────────────────────────────────────────────────────────
// AUTO-LOAD: fetch PDF + OCR text from the Inputs folder
// ─────────────────────────────────────────────────────────────────
async function loadWarDiary() {
  if (typeof pdfjsLib === 'undefined') {
    setStatus('PDF.js not loaded — check internet connection, then refresh.', true);
    return;
  }
  setStatus('Loading war diary…');
  showImgSpinner(true); showTxtSpinner(true);

  const [textRes, pdfRes] = await Promise.allSettled([
    fetch(REAL_TEXT_PATH + '?v=' + Date.now()).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }),
    fetch(REAL_PDF_PATH ).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); }),
  ]);

  // OCR text
  showTxtSpinner(false);
  if (textRes.status === 'fulfilled') {
    loadOCRText(textRes.value, true);
  } else {
    showTxtPlaceholder('Could not load OCR text: ' + textRes.reason + '.<br><em>Use the OCR Override picker above.</em>');
  }

  // PDF
  showImgSpinner(false);
  if (pdfRes.status === 'fulfilled') {
    await loadPDFBuffer(pdfRes.value);
    setStatus(
      state.totalPages + '-page PDF loaded' +
      (textRes.status === 'fulfilled' ? ' + OCR text' : '') +
      '. Use ◀▶ to navigate.'
    );
  } else {
    showImgPlaceholder('Could not load PDF: ' + pdfRes.reason + '.<br><em>Use the PDF Override picker above.</em>');
    if (textRes.status === 'fulfilled') {
      setStatus('OCR text loaded. PDF unavailable — use the PDF Override picker.', true);
    }
  }
}

// ─────────────────────────────────────────────────────────────────
// PDF.js — load & render
// ─────────────────────────────────────────────────────────────────
async function loadPDFBuffer(arrayBuffer) {
  try {
    state.pdfDoc   = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
    state.totalPages = state.pdfDoc.numPages;
    state.mode       = 'pdf';
    state.currentPage = 1;
    state.wordData    = [];   // clear JSON sidecar — use PDF text layer
    pageLabel.textContent = '/ ' + state.totalPages;
    // Note: pageInput.value is set by renderCurrentPage() below.
    // Setting it here would fire a change event → goToPage → duplicate render.
    btnPrev.disabled = true;
    btnNext.disabled = state.totalPages <= 1;
    pdfContainer.style.display = 'inline-block';
    imagePlaceholder.style.display = 'none';
    await renderCurrentPage();
  } catch (err) {
    showImgPlaceholder('Failed to open PDF: ' + err.message);
  }
}

async function renderCurrentPage() {
  if (!state.pdfDoc || state.rendering) return;
  // Debounce: ignore spurious re-render calls (e.g. from resize events) that
  // fire within 100 ms of a render that just completed.
  if (Date.now() - state.lastRenderTime < 100) return;
  state.rendering = true;
  try {
    const page = await state.pdfDoc.getPage(state.currentPage);

    // Fit page to panel width, then apply zoomScale. Multiply by DPR for sharp HiDPI rendering.
    const dpr      = window.devicePixelRatio || 1;
    const panelW   = document.getElementById('image-panel-body').clientWidth || 700;
    const baseVP   = page.getViewport({ scale: 1 });
    const fitScale = panelW / baseVP.width;
    const vp       = page.getViewport({ scale: fitScale * state.zoomScale * dpr });

    // Physical canvas size = DPR × CSS size → crisp on Retina/HiDPI displays
    pdfCanvas.width  = vp.width;
    pdfCanvas.height = vp.height;
    pdfCanvas.style.width  = (vp.width  / dpr) + 'px';
    pdfCanvas.style.height = (vp.height / dpr) + 'px';

    await page.render({ canvasContext: pdfCanvas.getContext('2d'), viewport: vp }).promise;
    await buildPDFWordOverlay(page, vp, dpr);

    pageInput.value   = state.currentPage;
    btnPrev.disabled  = state.currentPage <= 1;
    btnNext.disabled  = state.currentPage >= state.totalPages;

    // Sync right panel
    try {
      showOCRPage(state.currentPage);
    } catch (e) {
      setStatus('OCR panel error on page ' + state.currentPage + ': ' + e.message, true);
      console.error(e);
    }

    state.lastRenderTime = Date.now();   // debounce: record when this render finished
    _lastRenderedZoom    = state.zoomScale;  // keep transform delta in sync
  } finally {
    state.rendering = false;
    // If the user navigated while we were rendering, honour the queued page
    if (_pendingPage !== null) {
      const next = _pendingPage;
      _pendingPage = null;
      if (next !== state.currentPage) {
        state.currentPage = next;
        renderCurrentPage();
      }
    }
  }
}

// Build word-box overlay from PDF.js text content
async function buildPDFWordOverlay(page, vp, dpr) {
  dpr = dpr || 1;
  overlay.innerHTML = '';
  // Overlay must match CSS canvas size, not physical pixel size
  overlay.style.width  = (vp.width  / dpr) + 'px';
  overlay.style.height = (vp.height / dpr) + 'px';

  // JSON sidecar in PDF mode — coords are PDF points (bottom-left origin)
  if (state.wordData.length) {
    state.wordData.forEach(e => {
      const [cx, cy] = vp.convertToViewportPoint(e.bbox.x, e.bbox.y + e.bbox.h);
      const cw = e.bbox.w * vp.scale;
      const ch = e.bbox.h * vp.scale;
      overlay.appendChild(makeWordBox(e.id, cx/dpr, cy/dpr, cw/dpr, ch/dpr, e.word));
    });
  }
  // No JSON sidecar — overlay intentionally left blank
}

function makeWordBox(id, x, y, w, h, title) {
  const div = document.createElement('div');
  div.className = 'word-box';
  div.dataset.wordId = id;
  div.style.left   = Math.round(x) + 'px';
  div.style.top    = Math.round(y) + 'px';
  div.style.width  = Math.max(4, Math.round(w)) + 'px';
  div.style.height = Math.max(4, Math.round(h)) + 'px';
  if (title) div.title = title;
  div.addEventListener('mouseenter', () => { div.classList.add('highlight'); });
  div.addEventListener('mouseleave', () => { div.classList.remove('highlight'); });
  return div;
}

let _pendingPage = null;   // holds the last page requested while a render was in progress

// ─────────────────────────────────────────────────────────────────
// OCR TEXT RENDERING
// ─────────────────────────────────────────────────────────────────
function loadOCRText(rawText, isHtmlMarkdown) {
  textPlaceholder.style.display = 'none';

  if (isHtmlMarkdown) {
    // Parse the olmOCR markdown into a per-page dictionary so we can render
    // exactly one page at a time — fast, accurate, and avoids the "highlight all"
    // problem that comes from searching a giant single-document DOM.
    state.ocrPages = parseOCRByPage(rawText);
    // Do NOT call showOCRPage() here. If the PDF is already loaded,
    // renderCurrentPage() will call showOCRPage() as part of its own flow.
    // If the PDF isn't loaded yet (the common startup path where OCR arrives
    // first), renderCurrentPage() will call showOCRPage() once it runs.
    // Calling it here would build the OCR panel twice on every startup.
    if (state.pdfDoc) {
      showOCRPage(state.currentPage || 1);
    }
    // else: leave the panel blank — renderCurrentPage() will populate it.
  } else {
    // Plain-text / sample mode — load directly into editor
    state.ocrPages = {};
    textEditor.value = rawText;
    updateLineNumbers();
  }
}

// ── Parse olmOCR markdown into per-page dict ──────────────────────────────
// Each "## Page N" heading starts a new page entry. Content between two
// headings (tables, prose, weather notes) is stored under its page number.
function parseOCRByPage(rawText) {
  const pages = {};
  const lines  = rawText.split('\n');
  let pageNum  = null;
  let buf      = [];

  for (const line of lines) {
    const m = line.match(/^## Page (\d+)\s*$/);
    if (m) {
      if (pageNum !== null) pages[pageNum] = buf.join('\n');
      pageNum = parseInt(m[1], 10);
      buf     = [];           // do NOT include the ## Page N heading in the content
    } else if (pageNum !== null) {
      buf.push(line);
    }
  }
  if (pageNum !== null) pages[pageNum] = buf.join('\n');
  return pages;
}

function showOCROverlay(pageNum) {
  const corr        = state.corrections && state.corrections[pageNum];
  const originalText = (corr && corr.original) || state.ocrPages[pageNum] || '';

  ocrOverlayPanel.innerHTML =
    '<pre style="white-space:pre-wrap;font-family:inherit;font-size:13px;' +
    'line-height:1.8;color:var(--text);margin:0;">' +
    escH(originalText) + '</pre>';

  ocrOverlayPanel.style.display  = 'block';
  pdfContainer.style.display     = 'none';
  imagePlaceholder.style.display = 'none';
}

function hideOCROverlay() {
  ocrOverlayPanel.style.display = 'none';
  if (state.mode === 'pdf') {
    pdfContainer.style.display   = 'block';
  } else {
    imagePlaceholder.style.display = 'flex';
  }
}

function toggleOCROverlay() {
  state.ocrOverlay = !state.ocrOverlay;
  btnOcrToggle.classList.toggle('active', state.ocrOverlay);

  if (state.ocrOverlay) {
    showOCROverlay(state.currentPage);
  } else {
    hideOCROverlay();
  }
}

btnOcrToggle.addEventListener('click', toggleOCROverlay);

// ── Render the OCR panel for a single page ────────────────────────────────
function showOCRPage(pageNum) {
  textEditor.dataset.page = pageNum;

  if (!Object.keys(state.ocrPages).length) {
    textEditor.value = '';
    ocrSyncLabel.textContent = '';
    return;
  }

  const content = state.ocrPages[pageNum];
  if (!content) {
    textEditor.value = 'No OCR text for page ' + pageNum + '.';
    ocrSyncLabel.textContent = 'page ' + pageNum + ' — no data';
    updateLineNumbers();
    return;
  }

  const corr = state.corrections && state.corrections[pageNum];
  const editText = corr
    ? (currentRole === 'pass2'
        ? (corr.pass2 || corr.pass1 || corr.original || content)
        : (corr.pass1 || corr.original || content))
    : content;

  textEditor.value = editText;
  textEditor.scrollTop = 0;
  ocrSyncLabel.textContent = 'page ' + pageNum;

  updateLineNumbers();
  updateFlagButtons(pageNum);
  updateCorrectionIndicator(pageNum);

  // Snapshot original on first visit
  if (!state.corrections[pageNum] || !state.corrections[pageNum].original) {
    if (!state.corrections[pageNum]) state.corrections[pageNum] = {};
    state.corrections[pageNum].original = content.trim();
  }

  // Pass 1 done-page lock
  const doneBanner = document.getElementById('done-banner');
  const isDonePage = currentRole === 'pass1' && state.flags[pageNum] === 'done';
  if (isDonePage) {
    textEditor.readOnly = true;
    textEditor.style.opacity = '0.55';
    doneBanner.innerHTML = '✓ <strong>Done</strong> — submitted to Pass 2 queue. This page is locked.';
    doneBanner.style.display = 'flex';
  } else {
    textEditor.readOnly = false;
    textEditor.style.opacity = '';
    doneBanner.style.display = 'none';
  }

  if (currentRole === 'pass2') {
    updatePass2NavButtons();
    if (state.ocrOverlay) showOCROverlay(pageNum);
    showPass2RevertBar(pageNum);
    showPass2ChangeSummary(pageNum);
  }
}

// ─────────────────────────────────────────────────────────────────
// WORD DATA (JSON SIDECAR)
// ─────────────────────────────────────────────────────────────────
function loadWordDataJSON(data) {
  state.wordData = data;

  // PDF mode: re-render so buildPDFWordOverlay picks up the new boxes
  if (state.mode === 'pdf' && state.pdfDoc) {
    renderCurrentPage();
    return;
  }

  // Image mode: re-render overlay & re-link text spans (unchanged behaviour)
  if (state.mode === 'image' && pdfCanvas.width) {
    const sx = pdfCanvas.width  / state.imageNaturalW;
    const sy = pdfCanvas.height / state.imageNaturalH;
    overlay.innerHTML = '';
    overlay.style.width  = pdfCanvas.width  + 'px';
    overlay.style.height = pdfCanvas.height + 'px';
    data.forEach(e => overlay.appendChild(
      makeWordBox(e.id, e.bbox.x*sx, e.bbox.y*sy, e.bbox.w*sx, e.bbox.h*sy, e.word)
    ));
  }
}

// ─────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────
function escH(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function showImgSpinner(on) {
  imgSpinner.style.display = on ? 'inline-block' : 'none';
  imgPhText.style.display  = on ? 'none' : 'block';
  imagePlaceholder.style.display = 'flex';
  if (on) pdfContainer.style.display = 'none';
}
function showTxtSpinner(on) {
  txtSpinner.style.display = on ? 'inline-block' : 'none';
  txtPhText.style.display  = on ? 'none' : 'block';
  textPlaceholder.style.display = 'flex';
}
function showImgPlaceholder(html) {
  imgPhText.innerHTML = html;
  imagePlaceholder.style.display = 'flex';
  pdfContainer.style.display = 'none';
}
function showTxtPlaceholder(html) {
  txtPhText.innerHTML = html;
  textPlaceholder.style.display = 'flex';
}
function setStatus(msg, warn) {
  statusMsg.textContent = msg;
  statusMsg.style.color = warn ? '#e07070' : 'var(--text-dim)';
}

// ─────────────────────────────────────────────────────────────────
// PHASE 3 — SCROLL ZOOM ON LEFT PANEL
// ─────────────────────────────────────────────────────────────────
const ZOOM_SCALE_MIN  = 0.5;
const ZOOM_SCALE_MAX  = 4.0;
const ZOOM_SCALE_STEP = 0.1;

// Zoom state — separate "last rendered" zoom from "desired" zoom so we can
// use CSS transform for instant feedback without triggering a PDF re-render
// on every wheel tick.
let _lastRenderedZoom  = 1.0;
let _zoomSettleTimer   = null;
let _zoomAnchorFracX   = 0.5;
let _zoomAnchorFracY   = 0.5;
let _zoomAnchorClientX = 0;
let _zoomAnchorClientY = 0;

function updateZoomLabel() {
  const pct = Math.round(state.zoomScale * 100);
  zoomLabel.textContent = pct + '%';
  btnZoomReset.style.opacity = state.zoomScale === 1.0 ? '0.4' : '1';
}

imagePanelBody.addEventListener('wheel', e => {
  if (state.mode === 'none') return;
  if (state.ocrOverlay) return;   // overlay is showing — let scroll pass through normally
  e.preventDefault();

  const delta   = e.deltaY < 0 ? ZOOM_SCALE_STEP : -ZOOM_SCALE_STEP;
  const newZoom = Math.round(
    Math.max(ZOOM_SCALE_MIN, Math.min(ZOOM_SCALE_MAX, state.zoomScale + delta)) * 10
  ) / 10;

  if (newZoom === state.zoomScale) return;

  // Capture cursor anchor once at the start of each new gesture (when timer isn't running)
  if (_zoomSettleTimer === null) {
    const rect = imagePanelBody.getBoundingClientRect();
    const cssW = parseFloat(pdfCanvas.style.width)  || pdfCanvas.clientWidth  || 700;
    const cssH = parseFloat(pdfCanvas.style.height) || pdfCanvas.clientHeight || 900;
    _zoomAnchorClientX = e.clientX - rect.left;
    _zoomAnchorClientY = e.clientY - rect.top;
    _zoomAnchorFracX   = (_zoomAnchorClientX + imagePanelBody.scrollLeft) / cssW;
    _zoomAnchorFracY   = (_zoomAnchorClientY + imagePanelBody.scrollTop)  / cssH;
  }

  state.zoomScale = newZoom;
  updateZoomLabel();

  // Instant visual feedback via CSS transform — GPU-composited, zero layout impact,
  // zero flicker. Scale relative to the last full render so the factor is exact.
  const sf = state.zoomScale / _lastRenderedZoom;
  const ox = _zoomAnchorFracX * 100;
  const oy = _zoomAnchorFracY * 100;
  pdfCanvas.style.transformOrigin = `${ox}% ${oy}%`;
  pdfCanvas.style.transform       = `scale(${sf})`;
  overlay.style.transformOrigin   = `${ox}% ${oy}%`;
  overlay.style.transform         = `scale(${sf})`;

  // After the scroll gesture settles, do ONE proper HiDPI re-render
  clearTimeout(_zoomSettleTimer);
  _zoomSettleTimer = setTimeout(async () => {
    _zoomSettleTimer = null;

    // Clear the transform before re-render to avoid double-scaling
    pdfCanvas.style.transform       = '';
    pdfCanvas.style.transformOrigin = '';
    overlay.style.transform         = '';
    overlay.style.transformOrigin   = '';

    state.lastRenderTime = 0;

    if (state.pdfDoc) {
      await renderCurrentPage();
    }

    _lastRenderedZoom = state.zoomScale;

    // Scroll so the anchor point stays under the cursor
    const newCSSW = parseFloat(pdfCanvas.style.width)  || pdfCanvas.clientWidth;
    const newCSSH = parseFloat(pdfCanvas.style.height) || pdfCanvas.clientHeight;
    imagePanelBody.scrollLeft = _zoomAnchorFracX * newCSSW - _zoomAnchorClientX;
    imagePanelBody.scrollTop  = _zoomAnchorFracY * newCSSH - _zoomAnchorClientY;
  }, 150);
}, { passive: false });

btnZoomReset.addEventListener('click', async () => {
  if (state.zoomScale === 1.0) return;
  // Cancel any pending settle timer
  clearTimeout(_zoomSettleTimer);
  _zoomSettleTimer = null;
  // Clear any in-progress CSS transform
  pdfCanvas.style.transform       = '';
  pdfCanvas.style.transformOrigin = '';
  overlay.style.transform         = '';
  overlay.style.transformOrigin   = '';
  state.zoomScale = 1.0;
  updateZoomLabel();
  state.lastRenderTime = 0;
  await renderCurrentPage();
  _lastRenderedZoom = 1.0;
  imagePanelBody.scrollLeft = 0;
  imagePanelBody.scrollTop  = 0;
});

// ─────────────────────────────────────────────────────────────────
// ROLE SWITCHER — PASS 1 / PASS 2
// ─────────────────────────────────────────────────────────────────
const roleSelect      = document.getElementById('role-select');
const pass2Banner     = document.getElementById('pass2-banner');
const pass2BannerText = document.getElementById('pass2-banner-text');

let currentRole = 'pass1';
let pass2Flags  = {};

const PASS2_BTN_LABELS = { done: '✓ Pass 2 approved', problem: '⚠ Needs work' };
const PASS1_BTN_LABELS = { done: '✓ Done',             problem: '⚠ Problem'    };

function getVerifiedPages() {
  return Object.keys(state.flags)
    .filter(k => state.flags[k] === 'done')
    .map(Number)
    .sort((a, b) => a - b);
}

function updatePass2NavButtons() {
  const verified = getVerifiedPages();
  // If Pass 1 queue has no done pages yet, fall back to sequential navigation
  if (!verified.length) {
    btnPrev.disabled = !state.pdfDoc || state.currentPage <= 1;
    btnNext.disabled = !state.pdfDoc || state.currentPage >= state.totalPages;
    return;
  }
  if (!state.pdfDoc) { btnPrev.disabled = true; btnNext.disabled = true; return; }
  btnPrev.disabled = !verified.some(p => p < state.currentPage);
  btnNext.disabled = !verified.some(p => p > state.currentPage);
}

function updatePass2Banner() {
  const count = getVerifiedPages().length;
  pass2BannerText.textContent = count === 0
    ? 'Pass 2 mode · No pages in queue'
    : 'Pass 2 mode · ' + count + ' page' + (count === 1 ? '' : 's') + ' ready for review';
  updatePass2NavButtons();
}

function applyRole(role) {
  currentRole = role;
  localStorage.setItem('p44-role', role);
  roleSelect.value = role;
  if (role === 'pass2') {
    pass2Banner.style.display = 'flex';
    btnOcrToggle.style.display = 'inline-block';
    btnFlagDone.textContent    = PASS2_BTN_LABELS.done;
    btnFlagProblem.textContent = PASS2_BTN_LABELS.problem;
    updatePass2Banner();
  } else {
    pass2Banner.style.display = 'none';
    btnOcrToggle.style.display = 'none';
    state.ocrOverlay = false;
    btnOcrToggle.classList.remove('active');
    hideOCROverlay();
    btnFlagDone.textContent    = PASS1_BTN_LABELS.done;
    btnFlagProblem.textContent = PASS1_BTN_LABELS.problem;
    if (state.pdfDoc) {
      btnPrev.disabled = state.currentPage <= 1;
      btnNext.disabled = state.currentPage >= state.totalPages;
    }
  }
  updateFlagButtons(state.currentPage);
}

function savePass2Flags() {
  try { localStorage.setItem(LS_PASS2_FLAGS_KEY, JSON.stringify(pass2Flags)); } catch (_) {}
  fetch(SERVER_PASS2_ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(pass2Flags),
  }).catch(() => {});
}

function loadPass2FlagsOnStartup() {
  fetch('/' + SERVER_PASS2_FLAGS_FILE)
    .then(r => r.ok ? r.json() : Promise.reject())
    .then(data => { pass2Flags = data; })
    .catch(() => {
      try {
        const stored = localStorage.getItem(LS_PASS2_FLAGS_KEY);
        if (stored) pass2Flags = JSON.parse(stored);
      } catch (_) {}
    });
}

function resetPass2() {
  if (!confirm(
    'Reset Pass 2?\n\n' +
    'This will clear:\n' +
    '  • All Pass 2 approved / needs-work / rejected flags\n' +
    '  • All saved text corrections\n\n' +
    'Pass 1 done flags are NOT affected.\n\n' +
    'This cannot be undone.'
  )) return;

  // Wipe Pass 2 flags
  pass2Flags = {};
  try { localStorage.removeItem(LS_PASS2_FLAGS_KEY); } catch (_) {}

  // Wipe corrections (these are shared with Pass 1 editor but the contamination
  // source was Pass 2 auto-edit; clearing gives a clean slate)
  state.corrections = {};
  try { localStorage.removeItem(LS_CORRECTIONS_KEY); } catch (_) {}
  try { fetch(SERVER_CORRECTIONS_END, { method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({}) }); } catch (_) {}

  // Refresh UI
  updateFlagButtons(state.currentPage);
  updateCorrectionIndicator(state.currentPage);
  updatePass2Banner();
  if (state.pdfDoc) showOCRPage(state.currentPage);
  setStatus('Pass 2 queue reset — all Pass 2 flags and corrections cleared.');
}

document.getElementById('btn-pass2-reset').addEventListener('click', resetPass2);

// ─────────────────────────────────────────────────────────────────
// PASS 2 — CHANGE SUMMARY PANEL (Step 9)
// ─────────────────────────────────────────────────────────────────

// Walk both word arrays simultaneously and produce a list of changes.
// Simple sequential diff — fast, good enough for plain-text corrections.
function buildChangeSummary(originalText, pass1Text) {
  const orig = (originalText || '').trim().split(/\s+/).filter(Boolean);
  const edit = (pass1Text   || '').trim().split(/\s+/).filter(Boolean);
  if (orig.join(' ') === edit.join(' ')) return [];

  const changes = [];
  let i = 0, j = 0;
  while (i < orig.length || j < edit.length) {
    if (changes.length >= 50) { changes.push({ type: 'truncated' }); break; }
    if      (i >= orig.length) { changes.push({ type: 'added',   word: edit[j++] }); }
    else if (j >= edit.length) { changes.push({ type: 'deleted', word: orig[i++] }); }
    else if (orig[i] === edit[j]) { i++; j++; }
    else                       { changes.push({ type: 'changed', from: orig[i++], to: edit[j++] }); }
  }
  return changes;
}

function showPass2ChangeSummary(pageNum) {
  const panel = document.getElementById('pass2-change-summary');
  if (!panel) return;

  const corr = state.corrections && state.corrections[pageNum];
  if (!corr || !corr.pass1 || currentRole !== 'pass2') {
    panel.style.display = 'none';
    return;
  }

  const changes = buildChangeSummary(corr.original || state.ocrPages[pageNum] || '', corr.pass1);
  if (!changes.length) { panel.style.display = 'none'; return; }

  const truncated   = changes.find(c => c.type === 'truncated');
  const realChanges = changes.filter(c => c.type !== 'truncated');
  const count       = truncated ? '50+' : realChanges.length;

  const listHtml = realChanges.map(c => {
    if (c.type === 'changed') return `<span class="cs-changed">~ "${escH(c.from)}" → "${escH(c.to)}"</span>`;
    if (c.type === 'added')   return `<span class="cs-added">+ "${escH(c.word)}"</span>`;
    if (c.type === 'deleted') return `<span class="cs-deleted">− "${escH(c.word)}"</span>`;
    return '';
  }).join('<br>');

  panel.innerHTML =
    '<div class="cs-title">' + count + ' change' + (count === 1 ? '' : 's') + ' from Pass 1</div>' +
    '<div class="cs-list">' + listHtml + (truncated ? '<br><em style="font-size:10px;color:var(--text-muted)">…and more</em>' : '') + '</div>' +
    '<div class="cs-actions">' +
      '<button class="cs-revert-btn" id="btn-cs-revert-pass1">↺ Revert to Pass 1</button>' +
      '<button class="cs-revert-btn danger" id="btn-cs-revert-original">⚠ Revert to original OCR</button>' +
    '</div>';
  panel.style.display = 'block';

  const pass1Text = corr.pass1;

  document.getElementById('btn-cs-revert-pass1').addEventListener('click', () => {
    if (!confirm('Revert to Pass 1 text?\n\nYour Pass 2 changes on this page will be lost.')) return;
    textEditor.value = pass1Text;
    setStatus('Reverted to Pass 1 text — navigate away or click Edit to save.');
  });

  document.getElementById('btn-cs-revert-original').addEventListener('click', () => {
    const typed = prompt(
      'This will discard ALL corrections on this page — both Pass 1 and Pass 2.\n\nType REVERT to confirm:'
    );
    if (typed !== 'REVERT') { setStatus('Revert cancelled.'); return; }
    textEditor.value = state.ocrPages[pageNum] || '';
    delete state.corrections[pageNum];
    postCorrection(pageNum, null);
    updateCorrectionIndicator(pageNum);
    panel.style.display = 'none';
    setStatus('Reverted to original OCR — all corrections on this page cleared.');
  });
}

roleSelect.addEventListener('change', () => applyRole(roleSelect.value));

// ─────────────────────────────────────────────────────────────────
// PHASE 4 — FLAG / ANNOTATION SYSTEM
// ─────────────────────────────────────────────────────────────────
function updateFlagButtons(pageNum) {
  const flagSrc = currentRole === 'pass2' ? pass2Flags : state.flags;
  const flag = flagSrc[pageNum] || null;
  btnFlagDone.classList.toggle('active',    flag === 'done');
  btnFlagProblem.classList.toggle('active', flag === 'problem');

  // Done pages are read-only in Pass 1 — disable flag buttons
  const isDone = currentRole === 'pass1' && flag === 'done';
  btnFlagDone.disabled    = isDone;
  btnFlagProblem.disabled = isDone;
}

function updateFlagsSummary() {
  let done = 0, problem = 0;
  Object.values(state.flags).forEach(f => {
    if (f === 'done')    done++;
    else if (f === 'problem') problem++;
  });
  flagsSummary.textContent = done || problem
    ? '✓ ' + done + (problem ? '  ⚠ ' + problem : '')
    : '';
  if (currentRole === 'pass2') updatePass2Banner();
}

async function saveFlags() {
  try {
    const res = await fetch(SERVER_FLAGS_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(state.flags),
    });
    if (res.ok) return;
  } catch (_) {}
  // Server doesn't support it (or offline) — fall back to localStorage
  try { localStorage.setItem(LS_FLAGS_KEY, JSON.stringify(state.flags)); } catch (_) {}
}

function setFlag(type) {
  const p = state.currentPage;

  if (currentRole === 'pass2') {
    if (type === null) { delete pass2Flags[p]; } else { pass2Flags[p] = type; }
    updateFlagButtons(p);
    savePass2Flags();
    return;
  }

  // Pass 1 — Done is one-way: once set it cannot be cleared by the volunteer
  if (state.flags[p] === 'done') return;

  // Problem is toggleable — clicking it again removes it
  if (type === 'problem' && state.flags[p] === 'problem') {
    delete state.flags[p];
  } else if (type === null) {
    delete state.flags[p];
  } else {
    state.flags[p] = type;
  }

  updateFlagButtons(p);
  updateFlagsSummary();
  saveFlags();
  // Re-render page immediately so done-lock (readOnly) takes effect in Pass 1
  if (type === 'done' && currentRole === 'pass1') showOCRPage(state.currentPage);
}

btnFlagDone.addEventListener('click',    () => setFlag('done'));
btnFlagProblem.addEventListener('click', () => setFlag('problem'));

async function loadFlagsOnStartup() {
  try {
    const res = await fetch('/' + SERVER_FLAGS_FILE);
    if (res.ok) {
      state.flags = await res.json();
      updateFlagsSummary();
      return;
    }
  } catch (_) {}
  // Fallback: localStorage
  try {
    const stored = localStorage.getItem(LS_FLAGS_KEY);
    if (stored) { state.flags = JSON.parse(stored); updateFlagsSummary(); }
  } catch (_) {}
}

function clearPass1Queue() {
  const doneCount = Object.values(state.flags).filter(f => f === 'done').length;
  if (doneCount === 0) {
    setStatus('Pass 1 queue is already empty — no done pages to clear.');
    return;
  }
  if (!confirm(
    'Clear Pass 1 queue?\n\n' +
    'This will remove all ' + doneCount + ' done flag(s) from Pass 1.\n' +
    'The Pass 2 review queue will become empty.\n\n' +
    'Problem flags are NOT affected.\n' +
    'This cannot be undone.'
  )) return;

  // Remove all done flags; keep problem flags intact
  Object.keys(state.flags).forEach(k => {
    if (state.flags[k] === 'done') delete state.flags[k];
  });
  updateFlagButtons(state.currentPage);
  updateFlagsSummary();   // also calls updatePass2Banner if in pass2
  saveFlags();
  setStatus('Pass 1 queue cleared — ' + doneCount + ' done flag(s) removed. Pass 2 queue is now empty.');
}

document.getElementById('btn-clear-p1-queue').addEventListener('click', clearPass1Queue);

function clearAllLocalData() {
  if (!confirm(
    'Clear ALL saved data?\n\n' +
    'This will wipe:\n' +
    '  • All Pass 1 and Pass 2 flags\n' +
    '  • All text corrections\n' +
    '  • Role setting\n' +
    '  • Diff legend preference\n\n' +
    'This cannot be undone.'
  )) return;

  // Clear localStorage
  localStorage.removeItem(LS_CORRECTIONS_KEY);
  localStorage.removeItem(LS_FLAGS_KEY);
  localStorage.removeItem(LS_PASS2_FLAGS_KEY);
  localStorage.removeItem('p44-role');

  // Reset in-memory state
  state.corrections = {};
  state.flags       = {};
  pass2Flags        = {};

  // Reset role to Pass 1
  applyRole('pass1');

  // Clear server-side files (best-effort)
  fetch(SERVER_CLEAR_ENDPOINT, { method: 'POST' }).catch(() => {});

  // Refresh UI
  updateFlagButtons(state.currentPage);
  updateFlagsSummary();
  updateCorrectionIndicator(state.currentPage);
  if (state.pdfDoc) showOCRPage(state.currentPage);
  setStatus('All saved data cleared — fresh start.');
}

document.getElementById('btn-clear-all').addEventListener('click', clearAllLocalData);

// ─────────────────────────────────────────────────────────────────
// LINE NUMBERS — update and scroll-sync with textarea
// ─────────────────────────────────────────────────────────────────

// Returns a newline-joined string of line number labels that accounts for
// soft-wrapped visual rows in the textarea.  Wrapped continuation rows are
// represented by empty strings so the gutter stays in sync with scroll.
function buildWrappedLineNumbers(text, textarea) {
  const lines = text.split('\n');
  if (!lines.length) return '';

  // Lazily create a hidden canvas for text measurement (reused across calls).
  if (!buildWrappedLineNumbers._canvas) {
    buildWrappedLineNumbers._canvas = document.createElement('canvas');
  }
  const ctx   = buildWrappedLineNumbers._canvas.getContext('2d');
  const style = window.getComputedStyle(textarea);
  // Compose the CSS font shorthand that canvas expects
  ctx.font = style.fontSize + ' ' + style.fontFamily;

  // Width available for text inside the textarea (exclude horizontal padding).
  const paddingL   = parseFloat(style.paddingLeft)  || 0;
  const paddingR   = parseFloat(style.paddingRight) || 0;
  const availWidth = Math.max(50, textarea.clientWidth - paddingL - paddingR);

  const result = [];
  lines.forEach((line, i) => {
    result.push(String(i + 1));
    if (line.length > 0) {
      // Estimate the number of extra visual rows this line wraps onto.
      // We measure total line width and floor-divide by the available width.
      // This is an approximation — word-wrap boundaries differ slightly —
      // but it keeps the gutter aligned for typical OCR paragraph lengths.
      const lineWidth  = ctx.measureText(line).width;
      const extraRows  = Math.max(0, Math.floor(lineWidth / availWidth));
      for (let r = 0; r < extraRows; r++) result.push('');
    }
  });

  return result.join('\n');
}

function updateLineNumbers() {
  lineNumbers.textContent = buildWrappedLineNumbers(textEditor.value, textEditor);
  syncLineNumbers();
}

function syncLineNumbers() {
  lineNumbers.scrollTop = textEditor.scrollTop;
}

textEditor.addEventListener('input',  updateLineNumbers);
textEditor.addEventListener('scroll', syncLineNumbers);

// ─────────────────────────────────────────────────────────────────
// PHASE 5 — INLINE TEXT EDITING
// ─────────────────────────────────────────────────────────────────

function updateCorrectionIndicator(pageNum) {
  const corr = state.corrections && state.corrections[pageNum];
  const hasEdit = corr && (corr.pass1 || corr.pass2);
  ocrSyncLabel.style.color = hasEdit ? 'var(--gold)' : 'var(--gold-dim)';
}

async function postCorrection(pageNum, corrObj) {
  const body = JSON.stringify({ page: pageNum, correction: corrObj });
  try {
    const res = await fetch(SERVER_CORRECTIONS_END, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body,
    });
    if (res.ok) return;
  } catch (_) {}
  try {
    const stored = JSON.parse(localStorage.getItem(LS_CORRECTIONS_KEY) || '{}');
    if (corrObj) { stored[pageNum] = corrObj; } else { delete stored[pageNum]; }
    localStorage.setItem(LS_CORRECTIONS_KEY, JSON.stringify(stored));
  } catch (_) {}
}

async function loadCorrectionsOnStartup() {
  let loaded = null;
  try {
    const res = await fetch('/' + SERVER_CORRECTIONS_FILE);
    if (res.ok) loaded = await res.json();
  } catch (_) {}
  if (!loaded) {
    try {
      const stored = localStorage.getItem(LS_CORRECTIONS_KEY);
      if (stored) loaded = JSON.parse(stored);
    } catch (_) {}
  }
  if (loaded) {
    // Migrate old flat-string corrections to new object format
    Object.keys(loaded).forEach(k => {
      if (typeof loaded[k] === 'string') {
        if (loaded[k].trim()) {
          loaded[k] = { pass1: loaded[k].trim() };
        } else {
          delete loaded[k];
        }
      } else if (!loaded[k] || (!loaded[k].pass1 && !loaded[k].pass2)) {
        delete loaded[k];
      }
    });
    state.corrections = loaded;
    try { localStorage.setItem(LS_CORRECTIONS_KEY, JSON.stringify(state.corrections)); } catch (_) {}
  }
}

// ─────────────────────────────────────────────────────────────────
// PASS 2 REVERT BAR
// ─────────────────────────────────────────────────────────────────
function showPass2RevertBar(pageNum) {
  const bar = document.getElementById('pass2-revert-bar');
  if (currentRole !== 'pass2') { bar.style.display = 'none'; return; }

  const corr = state.corrections && state.corrections[pageNum];
  if (!corr || !corr.pass1) { bar.style.display = 'none'; return; }

  const originalText = corr.original || state.ocrPages[pageNum] || '';
  const pass1Text    = corr.pass1;

  bar.innerHTML =
    '<span class="rv-label">Pass 1 corrections on this page</span>' +
    '<button class="rv-btn" id="btn-revert-pass1">↺ Revert to Pass 1</button>' +
    '<button class="rv-btn danger" id="btn-revert-original">⚠ Revert to original OCR</button>';
  bar.style.display = 'flex';

  document.getElementById('btn-revert-pass1').addEventListener('click', () => {
    if (!confirm('Revert to Pass 1 text?\n\nYour Pass 2 changes on this page will be lost.')) return;
    textEditor.value = pass1Text;
    setStatus('Reverted to Pass 1 text — navigate away or click Edit to save.');
  });

  document.getElementById('btn-revert-original').addEventListener('click', () => {
    const typed = prompt(
      'This will discard ALL corrections on this page — both Pass 1 and Pass 2.\n\n' +
      'Type REVERT to confirm:'
    );
    if (typed !== 'REVERT') { setStatus('Revert cancelled.'); return; }
    textEditor.value = originalText;
    delete state.corrections[pageNum];
    postCorrection(pageNum, null);
    updateCorrectionIndicator(pageNum);
    bar.style.display = 'none';
    setStatus('Reverted to original OCR — all corrections on this page cleared.');
  });
}

function saveCurrentPage() {
  const pageNum    = parseInt(textEditor.dataset.page, 10);
  if (!pageNum) return;
  if (!state.ocrPages[pageNum]) return;  // do not save pages outside loaded MD range
  const editedText = textEditor.value.trim();
  const passKey    = currentRole === 'pass2' ? 'pass2' : 'pass1';
  const originalText = (state.corrections[pageNum] && state.corrections[pageNum].original)
    || (state.ocrPages[pageNum] || '').trim();

  if (!state.corrections[pageNum]) state.corrections[pageNum] = {};
  state.corrections[pageNum].original = state.corrections[pageNum].original || originalText;

  if (editedText && editedText !== originalText) {
    state.corrections[pageNum][passKey] = editedText;
  } else {
    delete state.corrections[pageNum][passKey];
  }

  if (!state.corrections[pageNum].pass1 && !state.corrections[pageNum].pass2) {
    delete state.corrections[pageNum];
  }

  postCorrection(pageNum, state.corrections[pageNum] || null);
  updateCorrectionIndicator(pageNum);
}

// ─────────────────────────────────────────────────────────────────
// PHASE 6 — KEYBOARD NAVIGATION
// ─────────────────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
  // Skip shortcuts when typing in the page-number input or the editor textarea
  const tag = document.activeElement && document.activeElement.tagName;
  if (tag === 'INPUT' && document.activeElement.id === 'page-input') return;
  if (tag === 'TEXTAREA') return;

  switch (e.key) {
    case 'ArrowRight': case 'l':
      e.preventDefault(); goToPage(state.currentPage + 1); break;
    case 'ArrowLeft': case 'j':
      e.preventDefault(); goToPage(state.currentPage - 1); break;
    case 'v':
      setFlag('done'); break;
    case 'p':
      setFlag('problem'); break;
  }
});

// ─────────────────────────────────────────────────────────────────
// HOOK INTO showOCRPage TO APPLY FLAGS/CORRECTIONS/EDIT STATE
// ─────────────────────────────────────────────────────────────────
// Override goToPage to save edits before navigating
function goToPage(n) {
  saveCurrentPage();

  if (!state.pdfDoc) return;
  n = Math.max(1, Math.min(state.totalPages, n));
  if (state.ocrOverlay) showOCROverlay(n);
  if (n === state.currentPage && !state.rendering) return;
  if (state.rendering) { _pendingPage = n; return; }
  state.currentPage = n;
  renderCurrentPage();
}

// ─────────────────────────────────────────────────────────────────
// STARTUP — load persistent data
// ─────────────────────────────────────────────────────────────────
loadPass2FlagsOnStartup();
loadFlagsOnStartup();
loadCorrectionsOnStartup();
// Restore saved role after all DOM refs are ready
(function() {
  const saved = localStorage.getItem('p44-role') || 'pass1';
  applyRole(saved);
})();










