'use strict';

(() => {

const api = window.stepforge;
const dialogs = window.StepForgeDialogs || {};

const clone = (value) => JSON.parse(JSON.stringify(value));
const BLOCK_KIND_ORDER = { text: 0, code: 1, table: 2 };

function blockText(block) {
  for (const key of ['code', 'text', 'body', 'value', 'content']) {
    const value = block && block[key];
    if (value != null && value !== '') return String(value);
  }
  return '';
}

function orderedStepBlocks(step) {
  const blocks = [];
  for (const tb of step.textBlocks || []) blocks.push({ kind: 'text', block: tb });
  for (const cb of step.codeBlocks || []) blocks.push({ kind: 'code', block: cb });
  for (const tbl of step.tableBlocks || []) blocks.push({ kind: 'table', block: tbl });
  return blocks.sort((a, b) => (
    (Number.isFinite(a.block.order) ? a.block.order : 0) - (Number.isFinite(b.block.order) ? b.block.order : 0)
    || BLOCK_KIND_ORDER[a.kind] - BLOCK_KIND_ORDER[b.kind]
    || String(a.block.id || '').localeCompare(String(b.block.id || ''))
  ));
}

function nextBlockOrder(step) {
  return orderedStepBlocks(step).reduce((max, entry) => Math.max(max, Number.isFinite(entry.block.order) ? entry.block.order : 0), 0) + 1;
}

function blockLabel(kind) {
  return kind === 'text' ? 'Text block' : kind === 'code' ? 'Code block' : 'Table';
}

function stepNumberMap(steps) {
  const numbers = new Map();
  const childCounts = new Map();
  let top = 0;
  for (const step of steps) {
    let number;
    if (step.parentStepId && numbers.has(step.parentStepId)) {
      const parent = numbers.get(step.parentStepId);
      const next = (childCounts.get(step.parentStepId) || 0) + 1;
      childCounts.set(step.parentStepId, next);
      number = `${parent}.${next}`;
    } else {
      top += 1;
      number = String(top);
    }
    numbers.set(step.stepId, number);
  }
  return numbers;
}

function isEditableTarget(target) {
  return target && (
    target.tagName === 'INPUT' ||
    target.tagName === 'TEXTAREA' ||
    target.isContentEditable
  );
}

class GuideEditor {
  constructor({ root, onMetaChange = () => {}, onToast = toast, onBack = () => {} } = {}) {
    this.root = root;
    this.onMetaChange = onMetaChange;
    this.onToast = onToast;
    this.onBack = onBack;

    this.guideId = null;
    this.guide = null;
    this.steps = [];
    this.stepMap = new Map();
    this.selectedStepId = null;
    this.stepSelectMode = false;
    this.selectedSteps = new Set();
    this.selectedAnnotationId = null;
    this.currentTool = 'select';
    this.currentZoom = 'fit';
    this.pendingSave = false;
    this.pendingGuideSave = false;
    this.canvasHistory = [];
    this.canvasFuture = [];
    this.beforeCanvasSnapshot = null;
    this.draggedBlock = null;
    this.stepLoadToken = 0;
    this.imageLoadToken = 0;
    this.shellMounted = false;
    this.linkedConflict = false;
    this.descriptionDirty = false;
    this.titleDirty = false;
    this.active = true;

    this.saveStepDebounced = debounce(() => this.flushStep(), 180);
    this.saveGuideDebounced = debounce(() => this.flushGuide(), 180);

    this.onDocumentKeyDown = this.onDocumentKeyDown.bind(this);
    document.addEventListener('keydown', this.onDocumentKeyDown, true);
  }

  destroy() {
    document.removeEventListener('keydown', this.onDocumentKeyDown, true);
    if (this.resizeObserver) this.resizeObserver.disconnect();
  }

  setActive(active) {
    this.active = Boolean(active);
  }

  get currentStep() {
    return this.stepMap.get(this.selectedStepId) || null;
  }

  get currentStepNumber() {
    if (!this.currentStep) return '';
    return stepNumberMap(this.steps).get(this.currentStep.stepId) || '';
  }

  getMeta() {
    return {
      guide: this.guide ? clone(this.guide) : null,
      step: this.currentStep ? clone(this.currentStep) : null,
      stepCount: this.steps.length,
      selectedStepId: this.selectedStepId,
      selectedAnnotationId: this.selectedAnnotationId,
      linked: Boolean(this.guide && this.guide.linkedSource),
      dirty: this.pendingSave || this.pendingGuideSave || this.descriptionDirty || this.titleDirty,
      view: 'editor',
    };
  }

  emitMeta() {
    this.onMetaChange(this.getMeta());
  }

  async open(guideId, stepId = null) {
    this.guideId = guideId;
    this.selectedStepId = stepId;
    this.selectedAnnotationId = null;
    this.canvasHistory = [];
    this.canvasFuture = [];
    this.pendingSave = false;
    this.pendingGuideSave = false;
    this.setActive(true);
    await this.reload(stepId);
  }

  async reload(stepId = this.selectedStepId) {
    const token = ++this.stepLoadToken;
    const { guide, steps } = await api.guide.get({ guideId: this.guideId });
    if (token !== this.stepLoadToken) return;
    this.guide = guide;
    this.steps = steps;
    this.stepMap = new Map(steps.map((step) => [step.stepId, step]));
    if (!this.shellMounted) this.mountShell();
    // An explicitly requested step (new capture, added step, restored
    // neighbour) wins; otherwise keep the current selection if it survived.
    if (stepId && this.stepMap.has(stepId)) {
      this.selectedStepId = stepId;
    } else if (!this.selectedStepId || !this.stepMap.has(this.selectedStepId)) {
      this.selectedStepId = (steps[0] && steps[0].stepId) || null;
    }
    this.selectedAnnotationId = null;
    this.renderAll();
  }

  mountShell() {
    this.shellMounted = true;
    this.root.innerHTML = '';
    const toolButtons = [
      ['select', 'Select'],
      ['rect', 'Rect'],
      ['oval', 'Oval'],
      ['line', 'Line'],
      ['arrow', 'Arrow'],
      ['text', 'Text'],
      ['tooltip', 'Tip'],
      ['number', '#'],
      ['blur', 'Blur'],
      ['highlight', 'Hi'],
      ['magnify', 'Mag'],
      ['cursor', 'Cursor'],
      ['crop', 'Crop'],
    ];

    this.dom = {};
    this.dom.root = el('div.editor', {},
      el('aside.pane-steps', {},
        el('div.pane-head', {},
          el('div', {},
            el('div.eyebrow', {}, 'Steps'),
            this.dom.stepCount = el('div.muted', {}, '0 steps'),
          ),
          el('div.row', {},
            this.dom.addStepBtn = el('button.primary', { type: 'button' }, 'Add'),
            this.dom.importBtn = el('button', { type: 'button' }, 'Import'),
            this.dom.selectStepsBtn = el('button', { type: 'button' }, 'Select'),
          ),
        ),
        this.dom.stepsList = el('div.steps-list'),
        this.dom.stepBulkBar = el('div'),
        this.dom.paneFoot = el('div.pane-foot', {},
          this.dom.moveUpBtn = el('button.icon', { type: 'button', title: 'Move step up' }, '↑'),
          this.dom.moveDownBtn = el('button.icon', { type: 'button', title: 'Move step down' }, '↓'),
          this.dom.duplicateBtn = el('button', { type: 'button' }, 'Duplicate'),
          this.dom.deleteBtn = el('button.danger', { type: 'button' }, 'Delete'),
        ),
      ),
      el('section.pane-canvas', {},
        el('div.canvas-toolbar', {},
          ...toolButtons.map(([tool, label]) => this.dom[`tool-${tool}`] = el('button.tool', { type: 'button', dataset: { tool } }, label)),
          el('span.sep'),
          this.dom.zoomFitBtn = el('button.tool', { type: 'button' }, 'Fit'),
          this.dom.zoom100Btn = el('button.tool', { type: 'button' }, '100%'),
          this.dom.zoom125Btn = el('button.tool', { type: 'button' }, '125%'),
          this.dom.zoom150Btn = el('button.tool', { type: 'button' }, '150%'),
          el('span.sep'),
          this.dom.undoBtn = el('button.tool', { type: 'button' }, 'Undo'),
          this.dom.redoBtn = el('button.tool', { type: 'button' }, 'Redo'),
        ),
        this.dom.canvasWrap = el('div.canvas-wrap', {},
          this.dom.canvas = el('canvas', { width: 1, height: 1 }),
          this.dom.canvasEmpty = el('div.canvas-empty', {}, 'Select an image step to edit annotations.'),
        ),
      ),
      el('aside.pane-props', {},
        el('section', {},
          el('h3', {}, 'Step'),
          this.dom.titleInput = el('input', { type: 'text', placeholder: 'Step title' }),
          this.dom.statusSelect = makeSelect('todo', [
            { value: 'todo', label: 'Todo' },
            { value: 'in-progress', label: 'In progress' },
            { value: 'done', label: 'Done' },
          ]),
          el('div.row', {},
            this.dom.hiddenToggle = el('label', {}, el('input', { type: 'checkbox' }), ' Hidden'),
            this.dom.skippedToggle = el('label', {}, el('input', { type: 'checkbox' }), ' Skipped'),
          ),
          el('div.row', {},
            this.dom.forceNewPageToggle = el('label', {}, el('input', { type: 'checkbox' }), ' New page'),
            this.dom.focusedViewToggle = el('label', {}, el('input', { type: 'checkbox' }), ' Focused'),
          ),
          this.dom.focusedControls = el('div.focused-controls.hidden', {},
            el('div.form-row', {}, el('label', {}, 'Zoom'),
              this.dom.fvZoom = el('input', { type: 'range', min: 1, max: 3, step: 0.05, value: 1.5 })),
            el('div.form-row', {}, el('label', {}, 'Pan X'),
              this.dom.fvPanX = el('input', { type: 'range', min: 0, max: 1, step: 0.01, value: 0.5 })),
            el('div.form-row', {}, el('label', {}, 'Pan Y'),
              this.dom.fvPanY = el('input', { type: 'range', min: 0, max: 1, step: 0.01, value: 0.5 })),
            el('div.muted', {}, 'Exports crop to this view; the original image is never modified.'),
          ),
        ),
        el('section', {},
          el('h3', {}, 'Description'),
          this.dom.richToolbar = el('div.rich-toolbar', {},
            this.toolbarBtn('bold', 'Bold'),
            this.toolbarBtn('italic', 'Italic'),
            this.toolbarBtn('insertUnorderedList', 'Bullet'),
            this.toolbarBtn('insertOrderedList', 'Number'),
            this.toolbarBtn('formatBlock', 'Quote', 'blockquote'),
            this.toolbarBtn('createLink', 'Link'),
            this.toolbarBtn('removeFormat', 'Clear'),
          ),
          this.dom.descEditor = el('div.rich-editor', { contentEditable: 'true', spellcheck: true }),
        ),
        el('section', {},
          el('h3', {}, 'Annotations'),
          this.dom.annotationList = el('div', { className: 'annotation-list' }),
          this.dom.annotationEditor = el('div', { className: 'annotation-editor' }),
        ),
        el('section', {},
          el('h3', {}, 'Blocks'),
          this.dom.blocksList = el('div', { className: 'blocks-list' }),
          el('div.row', {},
            this.dom.addTextBlockBtn = el('button', { type: 'button' }, '+ Text block'),
            this.dom.addCodeBlockBtn = el('button', { type: 'button' }, '+ Code'),
            this.dom.addTableBlockBtn = el('button', { type: 'button' }, '+ Table'),
          ),
        ),
        el('section', {},
          el('h3', {}, 'Guide'),
          this.dom.guideSummary = el('div.muted', {}),
          this.dom.linkedBtn = el('button', { type: 'button' }, 'Linked guide'),
          this.dom.saveNowBtn = el('button.primary', { type: 'button' }, 'Save now'),
          this.dom.snapshotBtn = el('button', { type: 'button' }, 'Snapshot'),
        ),
      ),
    );
    this.root.append(this.dom.root);

    // canvas interactions need to snapshot the current step before the drag
    // mutates it, so undo can restore the pre-edit annotations.
    this.dom.canvas.addEventListener('pointerdown', () => {
      if (this.currentStep) this.beforeCanvasSnapshot = { step: clone(this.currentStep) };
    }, true);

    this.canvas = new AnnotationCanvas(this.dom.canvas, {
      onChange: (annotations) => this.onCanvasChange(annotations),
      onSelect: (ann) => this.onCanvasSelect(ann),
      onCrop: (rect) => this.onCanvasCrop(rect),
      onRequestText: (ann) => this.editAnnotationText(ann),
      defaultStyle: (tool) => this.defaultStyleForTool(tool),
      nextNumber: () => this.nextAnnotationNumber(),
    });

    this.resizeObserver = new ResizeObserver(() => this.canvas.applyZoom());
    this.resizeObserver.observe(this.dom.canvasWrap);

    this.bindShellEvents();
  }

  toolbarBtn(action, label, block = null) {
    return el('button', {
      type: 'button',
      onClick: () => this.formatDescription(action, block),
    }, label);
  }

  bindShellEvents() {
    this.dom.addStepBtn.addEventListener('click', () => this.addEmptyStep());
    this.dom.importBtn.addEventListener('click', () => this.importImageSteps());
    this.dom.selectStepsBtn.addEventListener('click', () => this.toggleStepSelectMode());
    this.dom.moveUpBtn.addEventListener('click', () => this.moveSelectedStep(-1));
    this.dom.moveDownBtn.addEventListener('click', () => this.moveSelectedStep(1));
    this.dom.duplicateBtn.addEventListener('click', () => this.duplicateSelectedStep());
    this.dom.deleteBtn.addEventListener('click', () => this.deleteSelectedStep());
    this.dom.saveNowBtn.addEventListener('click', () => this.saveAll());
    this.dom.snapshotBtn.addEventListener('click', () => this.createSnapshot());
    this.dom.linkedBtn.addEventListener('click', () => this.openLinkedGuide());
    this.dom.zoomFitBtn.addEventListener('click', () => this.setZoom('fit'));
    this.dom.zoom100Btn.addEventListener('click', () => this.setZoom(1));
    this.dom.zoom125Btn.addEventListener('click', () => this.setZoom(1.25));
    this.dom.zoom150Btn.addEventListener('click', () => this.setZoom(1.5));
    this.dom.undoBtn.addEventListener('click', () => this.undo());
    this.dom.redoBtn.addEventListener('click', () => this.redo());

    Object.entries(this.dom).forEach(([key, value]) => {
      if (key.startsWith('tool-')) {
        value.addEventListener('click', () => this.setTool(value.dataset.tool));
      }
    });

    this.dom.titleInput.addEventListener('focus', () => {
      if (this.currentStep) this.pushCanvasHistory('title');
    });
    this.dom.titleInput.addEventListener('input', () => {
      if (!this.currentStep) return;
      this.currentStep.title = this.dom.titleInput.value;
      this.pendingSave = true;
      this.saveStepDebounced();
      this.renderStepList();
      this.emitMeta();
    });

    this.dom.statusSelect.addEventListener('change', () => {
      if (!this.currentStep) return;
      this.currentStep.status = this.dom.statusSelect.value;
      this.pendingSave = true;
      this.saveStepDebounced();
      this.renderStepList();
      this.emitMeta();
    });

    const bindCheckbox = (node, field) => node.addEventListener('change', () => {
      if (!this.currentStep) return;
      this.currentStep[field] = node.checked;
      this.pendingSave = true;
      this.saveStepDebounced();
      this.renderStepList();
      this.emitMeta();
    });
    bindCheckbox(this.dom.hiddenToggle.querySelector('input'), 'hidden');
    bindCheckbox(this.dom.skippedToggle.querySelector('input'), 'skipped');
    bindCheckbox(this.dom.forceNewPageToggle.querySelector('input'), 'forceNewPage');

    // Focused view lives under step.focusedView.enabled, not a flat field.
    const focusedInput = this.dom.focusedViewToggle.querySelector('input');
    focusedInput.addEventListener('change', () => {
      if (!this.currentStep) return;
      this.currentStep.focusedView = {
        zoom: 1.5, panX: 0.5, panY: 0.5,
        ...(this.currentStep.focusedView || {}),
        enabled: focusedInput.checked,
      };
      this.pendingSave = true;
      this.saveStepDebounced();
      this.syncFocusedControls();
      this.emitMeta();
    });
    const bindFocusedSlider = (node, field) => node.addEventListener('input', () => {
      const step = this.currentStep;
      if (!step || !step.focusedView) return;
      step.focusedView[field] = Number(node.value);
      this.pendingSave = true;
      this.saveStepDebounced();
    });
    bindFocusedSlider(this.dom.fvZoom, 'zoom');
    bindFocusedSlider(this.dom.fvPanX, 'panX');
    bindFocusedSlider(this.dom.fvPanY, 'panY');

    this.dom.addTextBlockBtn.addEventListener('click', () => this.addBlock('text'));
    this.dom.addCodeBlockBtn.addEventListener('click', () => this.addBlock('code'));
    this.dom.addTableBlockBtn.addEventListener('click', () => this.addBlock('table'));

    this.dom.descEditor.addEventListener('focus', () => {
      if (this.currentStep) this.pushCanvasHistory('description');
    });
    this.dom.descEditor.addEventListener('input', () => {
      if (!this.currentStep) return;
      this.currentStep.descriptionHtml = this.dom.descEditor.innerHTML;
      this.pendingSave = true;
      this.saveStepDebounced();
      this.emitMeta();
    });

    this.dom.descEditor.addEventListener('paste', (e) => {
      // Keep pasted text simple; backend sanitization will handle the rest.
      e.preventDefault();
      const text = e.clipboardData.getData('text/plain');
      document.execCommand('insertText', false, text);
    });

    this.dom.annotationList.addEventListener('click', (e) => {
      const item = e.target.closest('[data-ann-id]');
      if (!item) return;
      this.canvas.select(item.dataset.annId);
    });
  }

  renderAll() {
    this.renderStepList();
    this.syncStepFields();
    this.syncFocusedControls();
    this.renderCanvas();
    this.renderAnnotationPanel();
    this.renderBlocksPanel();
    this.renderGuidePanel();
    this.emitMeta();
  }

  syncFocusedControls() {
    const fv = this.currentStep?.focusedView;
    const enabled = Boolean(fv && fv.enabled);
    this.dom.focusedControls.classList.toggle('hidden', !enabled);
    if (enabled) {
      this.dom.fvZoom.value = fv.zoom || 1.5;
      this.dom.fvPanX.value = fv.panX ?? 0.5;
      this.dom.fvPanY.value = fv.panY ?? 0.5;
    }
  }

  // ---- text / code / table blocks ----------------------------------------

  addBlock(kind) {
    const step = this.currentStep;
    if (!step) {
      this.onToast('Select a step first.', { error: true });
      return;
    }
    const id = `blk-${Date.now().toString(36)}`;
    if (kind === 'text') {
      step.textBlocks = step.textBlocks || [];
      step.textBlocks.push({ id, order: nextBlockOrder(step), position: 'after-description', level: 'info', title: '', descriptionHtml: '' });
    } else if (kind === 'code') {
      step.codeBlocks = step.codeBlocks || [];
      step.codeBlocks.push({ id, order: nextBlockOrder(step), language: '', code: '' });
    } else if (kind === 'table') {
      step.tableBlocks = step.tableBlocks || [];
      step.tableBlocks.push({ id, order: nextBlockOrder(step), rows: [['Column A', 'Column B'], ['', '']] });
    }
    this.pendingSave = true;
    this.saveStepDebounced();
    this.renderBlocksPanel();
  }

  renderBlocksPanel() {
    clearNode(this.dom.blocksList);
    const step = this.currentStep;
    if (!step) {
      this.dom.blocksList.append(el('div.muted', {}, 'Select a step to add blocks.'));
      return;
    }
    const save = () => {
      this.pendingSave = true;
      this.saveStepDebounced();
    };
    const moveBlock = (source, target) => {
      if (!source || !target || source.kind === target.kind && source.block.id === target.block.id) return;
      const swap = source.block.order;
      source.block.order = target.block.order;
      target.block.order = swap;
      save();
      this.renderBlocksPanel();
    };
    const removeBtn = (onRemove) => el('button.icon.danger', {
      type: 'button', title: 'Remove block',
      onClick: () => { onRemove(); save(); this.renderBlocksPanel(); },
    }, '✕');

    const blocks = orderedStepBlocks(step);
    for (const [index, entry] of blocks.entries()) {
      const { kind, block } = entry;
      const canMoveUp = index > 0;
      const canMoveDown = index < blocks.length - 1;
      const moveUp = () => moveBlock(entry, blocks[index - 1]);
      const moveDown = () => moveBlock(entry, blocks[index + 1]);

      const header = el('div.row', {},
        el('strong', {}, blockLabel(kind)),
        el('span.muted', {}, `#${Number.isFinite(block.order) ? block.order : index + 1}`),
        el('span.spacer'),
        el('button.icon', { type: 'button', title: 'Move block up', disabled: !canMoveUp, onClick: moveUp }, '↑'),
        el('button.icon', { type: 'button', title: 'Move block down', disabled: !canMoveDown, onClick: moveDown }, '↓'),
        removeBtn(() => {
          if (kind === 'text') step.textBlocks = (step.textBlocks || []).filter((b) => b !== block);
          else if (kind === 'code') step.codeBlocks = (step.codeBlocks || []).filter((b) => b !== block);
          else step.tableBlocks = (step.tableBlocks || []).filter((b) => b !== block);
        }),
      );

      const card = el('div.block-card', {
        draggable: true,
        onDragStart: (e) => {
          this.draggedBlock = entry;
          if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
        },
        onDragOver: (e) => {
          if (this.draggedBlock) e.preventDefault();
        },
        onDrop: (e) => {
          e.preventDefault();
          if (!this.draggedBlock) return;
          moveBlock(this.draggedBlock, entry);
          this.draggedBlock = null;
        },
        onDragEnd: () => {
          this.draggedBlock = null;
        },
      }, header);

      if (kind === 'text') {
        const position = makeSelect(block.position, [
          { value: 'before-title', label: 'Before title' },
          { value: 'after-title', label: 'After title' },
          { value: 'before-image', label: 'Before image' },
          { value: 'after-image', label: 'After image' },
          { value: 'before-description', label: 'Before description' },
          { value: 'after-description', label: 'After description' },
        ]);
        const level = makeSelect(block.level, [
          { value: 'info', label: 'Note' },
          { value: 'warn', label: 'Warning' },
          { value: 'error', label: 'Important' },
          { value: 'success', label: 'Tip' },
        ]);
        const title = el('input', { type: 'text', value: block.title || '', placeholder: 'Block title' });
        const body = el('textarea', { rows: 2, placeholder: 'Block text' });
        body.value = (block.descriptionHtml || '').replace(/<[^>]+>/g, '');
        position.addEventListener('change', () => { block.position = position.value; save(); });
        level.addEventListener('change', () => { block.level = level.value; save(); });
        title.addEventListener('input', () => { block.title = title.value; save(); });
        body.addEventListener('input', () => { block.descriptionHtml = `<p>${escapeHtml(body.value)}</p>`; save(); });
        card.append(
          el('div.row', {}, level, position),
          title,
          body,
        );
      } else if (kind === 'code') {
        const lang = el('input', { type: 'text', value: block.language || '', placeholder: 'Language (e.g. bash)' });
        const code = el('textarea', { rows: 3, placeholder: 'Code', spellcheck: false });
        code.value = blockText(block);
        code.style.fontFamily = 'monospace';
        lang.addEventListener('input', () => { block.language = lang.value; save(); });
        code.addEventListener('input', () => { block.code = code.value; save(); });
        card.append(lang, code);
      } else if (kind === 'table') {
        const grid = el('textarea', { rows: 3, placeholder: 'One row per line, cells separated by |', spellcheck: false });
        grid.value = (block.rows || []).map((r) => r.join(' | ')).join('\n');
        grid.addEventListener('input', () => {
          block.rows = grid.value.split('\n').filter((l) => l.trim() !== '')
            .map((line) => line.split('|').map((c) => c.trim()));
          save();
        });
        card.append(
          el('div.muted', {}, 'First line is the header row.'),
          grid,
        );
      }

      this.dom.blocksList.append(card);
    }

    if (!blocks.length) {
      this.dom.blocksList.append(el('div.muted', {}, 'Informational text, code, and table blocks can be reordered with drag handles or arrows.'));
    }
  }

  renderStepList() {
    const current = this.currentStep;
    const numbers = stepNumberMap(this.steps);
    clearNode(this.dom.stepsList);
    this.dom.stepCount.textContent = `${this.steps.length} step${this.steps.length === 1 ? '' : 's'}`;
    this.dom.selectStepsBtn.className = this.stepSelectMode ? 'primary' : '';
    for (const step of this.steps) {
      const number = numbers.get(step.stepId) || '';
      let depth = 0;
      let parent = step.parentStepId;
      while (parent && this.stepMap.has(parent)) {
        depth += 1;
        parent = this.stepMap.get(parent).parentStepId;
      }
      const selected = current && current.stepId === step.stepId;
      const checked = this.selectedSteps.has(step.stepId);
      const item = el('div.step-item', {
        className: `step-item${selected ? ' selected' : ''}${depth ? ' sub' : ''}${step.skipped ? ' skipped' : ''}${step.hidden ? ' hiddenstep' : ''}`,
        dataset: { stepId: step.stepId },
        onClick: () => {
          if (this.stepSelectMode) this.toggleStepSelection(step.stepId);
          else this.selectStep(step.stepId);
        },
        onContextMenu: (e) => {
          e.preventDefault();
          if (this.stepSelectMode) return;
          this.selectStep(step.stepId);
          contextMenu(e.clientX, e.clientY, [
            { label: 'Add substep', action: () => this.addSubstep(step.stepId) },
            { label: 'Duplicate step', action: () => this.duplicateSelectedStep() },
            'sep',
            { label: 'Move up', action: () => this.moveSelectedStep(-1) },
            { label: 'Move down', action: () => this.moveSelectedStep(1) },
            'sep',
            { label: 'Delete step', danger: true, action: () => this.deleteSelectedStep() },
          ]);
        },
      },
      this.stepSelectMode
        ? el('input', {
          type: 'checkbox',
          checked,
          onClick: (e) => e.stopPropagation(),
          onChange: () => this.toggleStepSelection(step.stepId),
        })
        : null,
      el('span.status-dot', { className: `status-dot status-${step.status}` }),
      el('span.num', {}, number || '•'),
      el('span.t', {}, step.title || 'Untitled step'),
      el('span.flags', {}, [
        step.parentStepId ? 'sub' : '',
        step.hidden ? 'hidden' : '',
        step.skipped ? 'skipped' : '',
      ].filter(Boolean).join(' · ')));
      this.dom.stepsList.append(item);
    }
    if (!this.steps.length) {
      this.dom.stepsList.append(el('div.empty-state', { style: { marginTop: '40px' } }, 'No steps yet.'));
    }
    this.renderStepBulkBar();
  }

  toggleStepSelectMode() {
    this.stepSelectMode = !this.stepSelectMode;
    this.selectedSteps = new Set();
    this.renderStepList();
  }

  toggleStepSelection(stepId) {
    if (this.selectedSteps.has(stepId)) this.selectedSteps.delete(stepId);
    else this.selectedSteps.add(stepId);
    this.renderStepList();
  }

  selectAllSteps() {
    this.selectedSteps = new Set(this.steps.map((s) => s.stepId));
    this.renderStepList();
  }

  clearStepSelection() {
    this.selectedSteps = new Set();
    this.renderStepList();
  }

  renderStepBulkBar() {
    clearNode(this.dom.stepBulkBar);
    this.dom.paneFoot.classList.toggle('hidden', this.stepSelectMode);
    if (!this.stepSelectMode) return;
    const n = this.selectedSteps.size;
    const allSelected = this.steps.length > 0 && n === this.steps.length;
    this.dom.stepBulkBar.append(
      el('div.bulk-bar', {},
        el('span', {}, `${n} step${n === 1 ? '' : 's'} selected`),
        el('span.spacer', {}),
        el('button', {
          type: 'button',
          onClick: () => (allSelected ? this.clearStepSelection() : this.selectAllSteps()),
        }, allSelected ? 'Clear' : 'Select all'),
        el('button.danger', { type: 'button', disabled: !n, onClick: () => this.deleteSelectedSteps() }, 'Delete'),
      ),
    );
  }

  async deleteSelectedSteps() {
    const ids = [...this.selectedSteps];
    if (!ids.length) return;
    const ok = await confirmDialog(`Delete ${ids.length} step${ids.length === 1 ? '' : 's'}?`, { danger: true, okLabel: 'Delete' });
    if (!ok) return;
    for (const stepId of ids) {
      await api.step.delete({ guideId: this.guideId, stepId });
    }
    this.stepSelectMode = false;
    this.selectedSteps = new Set();
    await this.reload(null);
    this.onToast(`${ids.length} step${ids.length === 1 ? '' : 's'} deleted.`);
  }

  syncStepFields() {
    const step = this.currentStep;
    const guide = this.guide;
    if (!step) {
      this.dom.titleInput.value = '';
      this.dom.descEditor.innerHTML = '';
      this.dom.statusSelect.value = 'todo';
      this.dom.hiddenToggle.querySelector('input').checked = false;
      this.dom.skippedToggle.querySelector('input').checked = false;
      this.dom.forceNewPageToggle.querySelector('input').checked = false;
      this.dom.focusedViewToggle.querySelector('input').checked = false;
      this.dom.guideSummary.textContent = guide ? guide.title : '';
      return;
    }
    if (document.activeElement !== this.dom.titleInput) this.dom.titleInput.value = step.title || '';
    if (document.activeElement !== this.dom.descEditor) this.dom.descEditor.innerHTML = step.descriptionHtml || '';
    this.dom.statusSelect.value = step.status || 'todo';
    this.dom.hiddenToggle.querySelector('input').checked = Boolean(step.hidden);
    this.dom.skippedToggle.querySelector('input').checked = Boolean(step.skipped);
    this.dom.forceNewPageToggle.querySelector('input').checked = Boolean(step.forceNewPage);
    this.dom.focusedViewToggle.querySelector('input').checked = Boolean(step.focusedView?.enabled);
    this.dom.guideSummary.textContent = guide
      ? `${guide.title} · ${guide.linkedSource ? 'linked' : 'local'} · ${this.steps.length} steps`
      : '';
  }

  async renderCanvas() {
    const step = this.currentStep;
    const token = ++this.imageLoadToken;
    this.canvas.setTool(this.currentTool);
    this.canvas.setZoom(this.currentZoom);
    if (!step || !step.image) {
      this.canvas.setImage(null, 0, 0);
      this.dom.canvasEmpty.classList.remove('hidden');
      return;
    }
    this.dom.canvasEmpty.classList.add('hidden');
    const src = await api.step.imagePath({
      guideId: this.guideId,
      stepId: step.stepId,
      which: 'working',
    });
    if (token !== this.imageLoadToken || !src) return;
    const img = new Image();
    img.onload = () => {
      if (token !== this.imageLoadToken) return;
      this.canvas.setImage(img, img.naturalWidth || img.width, img.naturalHeight || img.height);
      this.canvas.setAnnotations(step.annotations || []);
      this.canvas.setTool(this.currentTool);
      this.canvas.setZoom(this.currentZoom);
    };
    img.onerror = () => {
      if (token !== this.imageLoadToken) return;
      this.canvas.setImage(null, 0, 0);
      this.dom.canvasEmpty.classList.remove('hidden');
    };
    img.src = src;
  }

  renderAnnotationPanel() {
    clearNode(this.dom.annotationList);
    const step = this.currentStep;
    if (!step) {
      this.dom.annotationList.append(el('div.muted', {}, 'No step selected.'));
      clearNode(this.dom.annotationEditor);
      this.dom.annotationEditor.append(el('div.muted', {}, 'Select a step to edit annotations.'));
      return;
    }

    const anns = step.annotations || [];
    if (!anns.length) {
      this.dom.annotationList.append(el('div.muted', {}, 'No annotations yet. Pick a tool and drag on the canvas.'));
    } else {
      for (const ann of anns) {
        const selected = this.canvas.selected() && this.canvas.selected().id === ann.id;
        this.dom.annotationList.append(el('div.block-card', {
          dataset: { annId: ann.id },
          style: { cursor: 'pointer', borderColor: selected ? 'var(--accent)' : '' },
        },
        el('div.row', {}, el('strong', {}, ann.type), el('span.muted', {}, ann.text || ann.value || '')),
        el('div.muted', {}, `${ann.x.toFixed(3)}, ${ann.y.toFixed(3)} · ${ann.w.toFixed(3)} × ${ann.h.toFixed(3)}`)));
      }
    }

    const selected = this.canvas.selected();
    clearNode(this.dom.annotationEditor);
    if (!selected) {
      this.dom.annotationEditor.append(el('div.muted', {}, 'Select an annotation to edit its style.'));
      return;
    }

    const style = selected.style || {};
    const typeSelect = makeSelect(selected.type, [
      'rect', 'oval', 'line', 'arrow', 'text', 'tooltip', 'number', 'blur', 'highlight', 'magnify', 'cursor',
    ].map((type) => ({ value: type, label: type })));
    const textInput = el('input', { type: 'text', value: selected.text || '', placeholder: 'Annotation text' });
    const valueInput = el('input', { type: 'number', value: Number.isFinite(selected.value) ? selected.value : '', placeholder: 'Value' });
    const strokeInput = el('input', { type: 'color', value: style.stroke || '#E5484D' });
    const fillInput = el('input', { type: 'color', value: style.fill && style.fill !== 'transparent' ? style.fill : '#ffffff' });
    const strokeWidthInput = el('input', { type: 'number', min: 1, step: 1, value: style.strokeWidth || 3 });
    const fontSizeInput = el('input', { type: 'number', min: 0.01, step: 0.001, value: style.fontSize || 0.022 });
    const textColorInput = el('input', { type: 'color', value: style.textColor || '#ffffff' });
    const zoomInput = el('input', { type: 'number', min: 1, step: 0.1, value: selected.zoom || 2 });
    const radiusInput = el('input', { type: 'number', min: 1, step: 1, value: selected.radius || 8 });
    const tailInput = makeSelect(style.tail || 'bottom', [
      { value: 'bottom', label: 'Bottom' },
      { value: 'top', label: 'Top' },
      { value: 'left', label: 'Left' },
      { value: 'right', label: 'Right' },
    ]);

    // Light-weight apply: mutate the selected annotation, redraw, and let the
    // debounced save flush. Re-rendering the panel here would rebuild the
    // inputs and steal focus mid-keystroke, so only structural changes
    // (type/tail) pass rerender: true.
    const apply = (patch, { rerender = false } = {}) => {
      const ann = this.canvas.selected();
      if (!ann) return;
      Object.assign(ann, patch);
      this.beforeCanvasSnapshot = null;
      step.annotations = clone(this.canvas.annotations || []);
      this.pendingSave = true;
      this.canvas.render();
      this.saveStepDebounced();
      if (rerender) this.renderAnnotationPanel();
      this.emitMeta();
    };

    const annSection = el('div', { className: 'annotation-editor-inner' },
      labeledRow('Type', typeSelect),
      labeledRow('Text', textInput),
      labeledRow('Value', valueInput),
      labeledRow('Stroke', strokeInput),
      labeledRow('Fill', fillInput),
      labeledRow('Stroke width', strokeWidthInput),
      labeledRow('Font size', fontSizeInput),
      labeledRow('Text color', textColorInput),
      labeledRow('Zoom', zoomInput),
      labeledRow('Radius', radiusInput),
      labeledRow('Tail', tailInput),
      el('div.row', {},
        el('button', {
          type: 'button',
          onClick: () => {
            this.canvas.deleteSelected();
          },
        }, 'Delete annotation'),
      ),
      el('div.row', {},
        el('button', { type: 'button', title: 'Copy this style to every annotation of the same type in this step', onClick: () => this.applyStyleAcross('step') }, 'Style → step'),
        el('button', { type: 'button', title: 'Copy this style to every annotation of the same type in the whole guide', onClick: () => this.applyStyleAcross('guide') }, 'Style → guide'),
      ),
    );
    this.dom.annotationEditor.append(annSection);

    typeSelect.addEventListener('change', () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      apply({ type: typeSelect.value }, { rerender: true });
      if (ann.type === 'tooltip') this.editAnnotationText(ann);
    });
    textInput.addEventListener('focus', () => this.pushCanvasHistory('annotation-text'));
    textInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ text: textInput.value });
    });
    valueInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      const next = valueInput.value === '' ? null : Number(valueInput.value);
      await apply({ value: Number.isFinite(next) ? next : null });
    });
    strokeInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ style: { ...ann.style, stroke: strokeInput.value } });
    });
    fillInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ style: { ...ann.style, fill: fillInput.value } });
    });
    strokeWidthInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ style: { ...ann.style, strokeWidth: Number(strokeWidthInput.value || 1) } });
    });
    fontSizeInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ style: { ...ann.style, fontSize: Number(fontSizeInput.value || 0.022) } });
    });
    textColorInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ style: { ...ann.style, textColor: textColorInput.value } });
    });
    zoomInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ zoom: Number(zoomInput.value || 2) });
    });
    radiusInput.addEventListener('input', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ radius: Number(radiusInput.value || 8) });
    });
    tailInput.addEventListener('change', async () => {
      const ann = this.canvas.selected();
      if (!ann) return;
      await apply({ style: { ...ann.style, tail: tailInput.value } });
    });
  }

  renderGuidePanel() {
    if (!this.guide) return;
    this.dom.linkedBtn.textContent = this.guide.linkedSource ? 'Linked guide' : 'Local guide';
    this.dom.linkedBtn.disabled = !this.guide.linkedSource;
    this.dom.snapshotBtn.textContent = 'Snapshot';
  }

  defaultStyleForTool(tool) {
    switch (tool) {
      case 'highlight': return { fill: '#ffe066', stroke: '#ffbf00', strokeWidth: 1 };
      case 'tooltip': return { fill: '#111827', textColor: '#ffffff', stroke: '#111827', tail: 'bottom' };
      case 'number': return { fill: '#1f6feb', stroke: '#1f6feb', textColor: '#ffffff' };
      case 'blur': return { fill: 'transparent', stroke: '#9ca3af', strokeWidth: 2 };
      case 'cursor': return { fill: '#ffffff', stroke: '#111827', strokeWidth: 2 };
      default: return { fill: 'transparent', stroke: '#E5484D', strokeWidth: 3, textColor: '#ffffff' };
    }
  }

  nextAnnotationNumber() {
    const step = this.currentStep;
    if (!step) return 1;
    const nums = (step.annotations || []).filter((ann) => ann.type === 'number').map((ann) => Number(ann.value) || 0);
    return (nums.length ? Math.max(...nums) : 0) + 1;
  }

  setTool(tool) {
    this.currentTool = tool;
    this.canvas.setTool(tool);
    for (const [key, node] of Object.entries(this.dom)) {
      if (!key.startsWith('tool-')) continue;
      node.classList.toggle('active', node.dataset.tool === tool);
    }
  }

  setZoom(mode) {
    this.currentZoom = mode;
    this.canvas.setZoom(mode);
    this.canvas.applyZoom();
    const buttons = [this.dom.zoomFitBtn, this.dom.zoom100Btn, this.dom.zoom125Btn, this.dom.zoom150Btn];
    buttons.forEach((btn) => btn.classList.remove('active'));
    if (mode === 'fit') this.dom.zoomFitBtn.classList.add('active');
    if (mode === 1) this.dom.zoom100Btn.classList.add('active');
    if (mode === 1.25) this.dom.zoom125Btn.classList.add('active');
    if (mode === 1.5) this.dom.zoom150Btn.classList.add('active');
  }

  pushCanvasHistory(recordOrLabel = 'change') {
    if (!this.currentStep) return;
    const record = recordOrLabel && typeof recordOrLabel === 'object' && recordOrLabel.step
      ? recordOrLabel
      : { step: clone(this.currentStep) };
    this.canvasHistory.push(record);
    if (this.canvasHistory.length > 40) this.canvasHistory.shift();
    this.canvasFuture.length = 0;
    this.beforeCanvasSnapshot = null;
  }

  async snapshotCurrentStep(includeImage = false) {
    if (!this.currentStep) return null;
    const record = { step: clone(this.currentStep) };
    if (includeImage && this.currentStep.image) {
      const image = await this.currentStepImageToBase64(this.currentStep);
      if (image) record.image = image;
    }
    return record;
  }

  async restoreHistoryRecord(record) {
    if (!record || !record.step) return;
    const step = clone(record.step);
    this.selectedStepId = step.stepId;
    this.beforeCanvasSnapshot = null;
    this.saveStepDebounced.cancel();
    this.pendingSave = false;
    if (record.image && step.image) {
      const saved = await api.step.setWorkingImage({
        guideId: this.guideId,
        stepId: step.stepId,
        pngBase64: record.image.base64,
        size: record.image.size,
        step,
      });
      this.stepMap.set(saved.stepId, saved);
      const idx = this.steps.findIndex((s) => s.stepId === saved.stepId);
      if (idx >= 0) this.steps[idx] = saved;
    } else {
      await this.flushStep(step);
    }
  }

  async undo() {
    if (!this.currentStep) return;
    if (!this.canvasHistory.length) {
      this.onToast('Nothing to undo.');
      return;
    }
    const current = await this.snapshotCurrentStep(true);
    if (current) this.canvasFuture.push(current);
    const previous = this.canvasHistory.pop();
    await this.restoreHistoryRecord(previous);
    this.renderAll();
  }

  async redo() {
    if (!this.currentStep) return;
    if (!this.canvasFuture.length) {
      this.onToast('Nothing to redo.');
      return;
    }
    const current = await this.snapshotCurrentStep(true);
    if (current) this.canvasHistory.push(current);
    const next = this.canvasFuture.pop();
    await this.restoreHistoryRecord(next);
    this.renderAll();
  }

  async flushStep(step = this.currentStep) {
    if (!step) return;
    this.pendingSave = false;
    const saved = await api.step.save({ guideId: this.guideId, step });
    this.stepMap.set(saved.stepId, saved);
    // Keep the steps array in sync — it holds the objects the list renders.
    const idx = this.steps.findIndex((s) => s.stepId === saved.stepId);
    if (idx >= 0) this.steps[idx] = saved;
    if (this.selectedStepId === saved.stepId) {
      this.renderStepList();
      this.syncStepFields();
      this.canvas.setAnnotations(saved.annotations || []);
      // Rebuilding the annotation editor while the user is typing in one of
      // its inputs would steal focus, so skip it in that case.
      if (!this.dom.annotationEditor.contains(document.activeElement)) {
        this.renderAnnotationPanel();
      }
      this.emitMeta();
    }
    return saved;
  }

  async flushGuide() {
    if (!this.guide) return;
    this.pendingGuideSave = false;
    await api.guide.save({ guide: this.guide });
    this.emitMeta();
  }

  async saveAll() {
    if (this.currentStep) await this.flushStep();
    if (this.guide) await this.flushGuide();
    this.onToast('Saved.');
  }

  async createSnapshot() {
    if (!this.guideId) return;
    await api.snapshots.create({ guideId: this.guideId, label: 'manual' });
    this.onToast('Snapshot created.');
  }

  async selectStep(stepId) {
    if (!this.stepMap.has(stepId)) return;
    this.selectedStepId = stepId;
    this.selectedAnnotationId = null;
    this.canvas.select(null);
    this.syncStepFields();
    this.renderStepList();
    this.renderCanvas();
    this.renderAnnotationPanel();
    this.emitMeta();
  }

  async addEmptyStep() {
    const title = await dialogs.promptText({
      title: 'Add Step',
      label: 'Step title',
      value: '',
      placeholder: 'Untitled step',
    });
    if (title == null) return;
    const step = await api.step.add({
      guideId: this.guideId,
      fields: {
        kind: 'empty',
        title: title.trim() || 'Untitled step',
        status: 'todo',
      },
      position: this.steps.length,
    });
    await this.reload(step.stepId);
    this.onToast('Step added.');
  }

  async addSubstep(parentStepId = this.selectedStepId) {
    if (!parentStepId) return;
    const title = await dialogs.promptText({
      title: 'Add Substep',
      label: 'Substep title',
      value: '',
      placeholder: 'Untitled substep',
    });
    if (title == null) return;
    const parent = this.stepMap.get(parentStepId);
    const parentIndex = this.steps.findIndex((s) => s.stepId === parentStepId);
    const step = await api.step.add({
      guideId: this.guideId,
      fields: {
        kind: 'empty',
        title: title.trim() || 'Untitled substep',
        parentStepId,
        status: 'todo',
      },
      position: parentIndex + 1,
    });
    await this.reload(step.stepId);
    this.onToast(parent ? 'Substep added.' : 'Step added.');
  }

  async duplicateSelectedStep() {
    const step = this.currentStep;
    if (!step) return;
    const copy = clone(step);
    copy.stepId = undefined;
    copy.title = copy.title ? `${copy.title} copy` : 'Untitled step copy';
    const image = await this.currentStepImageToBase64();
    const newStep = await api.step.add({
      guideId: this.guideId,
      fields: {
        ...copy,
        image: undefined,
      },
      imageBase64: image ? image.base64 : null,
      size: image ? image.size : null,
      position: this.steps.findIndex((s) => s.stepId === step.stepId) + 1,
    });
    await this.reload(newStep.stepId);
    this.onToast('Step duplicated.');
  }

  async deleteSelectedStep() {
    const step = this.currentStep;
    if (!step) return;
    const ok = await confirmDialog(`Delete “${step.title || 'Untitled step'}”?`, { danger: true, okLabel: 'Delete' });
    if (!ok) return;
    await api.step.delete({ guideId: this.guideId, stepId: step.stepId });
    const next = this.steps[this.steps.findIndex((s) => s.stepId === step.stepId) + 1]
      || this.steps[this.steps.findIndex((s) => s.stepId === step.stepId) - 1]
      || null;
    await this.reload(next && next.stepId);
    this.onToast('Step deleted.');
  }

  async moveSelectedStep(delta) {
    const step = this.currentStep;
    if (!step) return;
    const idx = this.steps.findIndex((s) => s.stepId === step.stepId);
    const nextIdx = idx + delta;
    if (nextIdx < 0 || nextIdx >= this.steps.length) return;
    const order = this.steps.map((s) => s.stepId);
    const [item] = order.splice(idx, 1);
    order.splice(nextIdx, 0, item);
    await api.step.reorder({ guideId: this.guideId, order });
    await this.reload(step.stepId);
  }

  async importImageSteps() {
    const result = await api.step.importImage({ guideId: this.guideId });
    if (!result || !result.ok) return;
    const last = result.steps && result.steps[result.steps.length - 1];
    await this.reload(last ? last.stepId : this.selectedStepId);
    this.onToast('Images imported.');
  }

  async captureStep(mode, delayMs = null) {
    const result = mode === 'region'
      ? await api.capture.region({ guideId: this.guideId })
      : await api.capture.shoot({ guideId: this.guideId, mode, delayMs });
    if (result && result.ok) {
      await this.reload(result.step.stepId);
      this.onToast('Captured.');
    } else if (result && result.reason) {
      this.onToast(result.reason, { error: true });
    }
  }

  /** Capture menu anchored at a toolbar button. */
  async openCaptureMenu(event) {
    const rect = event.target.getBoundingClientRect();
    const session = (await api.capture.state())?.active;
    contextMenu(rect.left, rect.bottom + 4, [
      { label: 'Capture full screen', action: () => this.captureStep('fullscreen') },
      { label: 'Capture window', action: () => this.captureStep('window') },
      { label: 'Capture region…', action: () => this.captureStep('region') },
      'sep',
      { label: 'Paste image as step', action: () => this.pasteClipboardStep() },
      { label: 'Import images…', action: () => this.importImageSteps() },
      'sep',
      session
        ? { label: 'Finish capture session', action: () => this.finishCaptureSession() }
        : { label: 'Start capture session (hotkey)', action: () => this.startCaptureSession() },
    ]);
  }

  async pasteClipboardStep() {
    const result = await api.step.fromClipboard({ guideId: this.guideId });
    if (result && result.ok) {
      await this.reload(result.step.stepId);
      this.onToast('Image pasted as a new step.');
    } else {
      this.onToast(result?.reason || 'Clipboard has no image.', { error: true });
    }
  }

  async shareAsFile() {
    const result = await api.archive.export({ guideId: this.guideId });
    if (result && result.ok) this.onToast(`Shared to ${result.path}`);
  }

  async openBackupsDialog() {
    if (!this.guideId) return;
    const snapshots = await api.snapshots.list({ guideId: this.guideId });
    await dialogs.showBackupsDialog({
      snapshots,
      onCreate: async () => {
        await api.snapshots.create({ guideId: this.guideId, label: 'manual' });
        this.onToast('Snapshot created.');
        return api.snapshots.list({ guideId: this.guideId });
      },
      onRestore: async (name) => {
        const ok = await confirmDialog(
          `Restore "${name}"? Current state is snapshotted first, so this is undoable.`,
          { okLabel: 'Restore' },
        );
        if (!ok) return false;
        await api.snapshots.restore({ guideId: this.guideId, name });
        await this.reload();
        this.onToast('Snapshot restored.');
        return true;
      },
    });
  }

  async openGuidePlaceholders() {
    if (!this.guide) return;
    await dialogs.showPlaceholdersDialog({
      title: 'Guide placeholders',
      hint: 'Use [[Name]] in titles, descriptions, and blocks. Guide values override global ones.',
      values: this.guide.placeholders || {},
      onSave: async (values) => {
        this.guide.placeholders = values;
        await api.guide.save({ guide: this.guide });
        this.onToast('Placeholders saved.');
      },
    });
  }

  openShortcutsHelp() {
    dialogs.showShortcutsDialog();
  }

  /** Copy the selected annotation's style to every annotation of the same type. */
  async applyStyleAcross(scope) {
    const source = this.canvas.selected();
    if (!source) return;
    const patch = clone(source.style || {});
    if (scope === 'step') {
      const step = this.currentStep;
      for (const ann of step.annotations || []) {
        if (ann.type === source.type && ann.id !== source.id) ann.style = { ...ann.style, ...patch };
      }
      step.annotations = clone(step.annotations);
      await this.flushStep(step);
      this.onToast(`Style applied to all ${source.type} annotations in this step.`);
    } else {
      for (const step of this.steps) {
        let touched = false;
        for (const ann of step.annotations || []) {
          if (ann.type === source.type && ann.id !== source.id) {
            ann.style = { ...ann.style, ...patch };
            touched = true;
          }
        }
        if (touched || step.stepId === this.currentStep?.stepId) {
          await api.step.save({ guideId: this.guideId, step });
        }
      }
      await this.reload(this.selectedStepId);
      this.onToast(`Style applied to all ${source.type} annotations in the guide.`);
    }
  }

  async startCaptureSession() {
    await api.capture.session({ action: 'start', guideId: this.guideId });
    this.emitMeta();
  }

  async pauseCaptureSession() {
    await api.capture.session({ action: 'pause', guideId: this.guideId });
    this.onToast('Capture paused.');
    this.emitMeta();
  }

  async resumeCaptureSession() {
    await api.capture.session({ action: 'resume', guideId: this.guideId });
    this.onToast('Capture resumed.');
    this.emitMeta();
  }

  async finishCaptureSession() {
    await api.capture.session({ action: 'finish', guideId: this.guideId });
    this.onToast('Capture session finished.');
    this.emitMeta();
  }

  async openSettings() {
    const settings = await api.settings.all();
    const placeholders = await api.settings.globalPlaceholders();
    await dialogs.showSettingsDialog({
      settings,
      placeholders,
      onSave: async (next) => {
        await api.settings.set({ keyPath: 'appearance', value: next.appearance });
        await api.settings.set({ keyPath: 'spellcheck', value: next.spellcheck });
        await api.settings.set({ keyPath: 'capture', value: next.capture });
        await api.settings.set({ keyPath: 'editor', value: next.editor });
        await api.settings.set({ keyPath: 'exports', value: next.exports });
        await api.settings.set({ keyPath: 'backups', value: next.backups });
        await api.settings.setGlobalPlaceholders(next.placeholders || {});
      },
    });
  }

  async openExportDialog() {
    const formats = (await api.export.formats()).map((id) => ({ id, label: id.replace(/-/g, ' ') }));
    const templatesByFormat = {};
    for (const format of formats) {
      templatesByFormat[format.id] = await api.templates.list({ format: format.id });
    }
    const settings = await api.settings.all();
    await dialogs.showExportDialog({
      formats,
      templatesByFormat,
      defaultFormat: 'pdf',
      defaultOutDir: settings.exports?.lastOutputDirs?.pdf || '',
      onChooseDir: async (format) => api.export.chooseDir({ format }),
      onLoadDefaults: async (format) => api.export.defaults({ format }),
      onLoadTemplate: async (format, name) => api.templates.load({ format, name }),
      onSaveTemplate: async (format, name, options) => {
        await api.templates.save({ format, name, options });
        this.onToast(`Template “${name}” saved.`);
      },
      onManageTemplates: async (format, mode) => {
        if (mode === 'manage') {
          await dialogs.showTemplateManager({
            format,
            names: await api.templates.list({ format }),
            onRename: async (name, newName) => {
              await api.templates.rename({ format, name, newName });
              return api.templates.list({ format });
            },
            onDuplicate: async (name) => {
              await api.templates.duplicate({ format, name });
              return api.templates.list({ format });
            },
            onDelete: async (name) => {
              await api.templates.delete({ format, name });
              return api.templates.list({ format });
            },
            onImport: async () => {
              const res = await api.templates.import();
              if (res?.ok) this.onToast(`Imported template for ${res.format}.`);
              return api.templates.list({ format });
            },
            onExport: async (name) => {
              const res = await api.templates.export({ format, name });
              if (res?.ok) this.onToast('Template shared as .sfglt.');
            },
          });
        }
        return api.templates.list({ format });
      },
      onPreview: async ({ format, options }) => {
        const preview = await api.export.preview({ guideId: this.guideId, format, options });
        if (preview && preview.file) {
          await api.shell.openPath({ target: preview.file }); // open in default viewer
          this.onToast('Preview opened (first steps only).');
        }
        return true;
      },
      onExport: async ({ format, options, outDir }) => {
        const result = await api.export.run({ guideId: this.guideId, format, options, outDir });
        if (result && result.ok === false) return false;
        if (result && result.file) this.onToast(`Exported ${format}.`);
        return true;
      },
    });
  }

  async openLinkedGuide() {
    if (!this.guide || !this.guide.linkedSource) {
      await dialogs.showInfoDialog('Linked Guide', 'This guide is stored locally and is not linked to a shared archive.');
      return;
    }
    const library = await api.library.list();
    const guideMeta = library.guides.find((g) => g.guideId === this.guideId) || this.guide;
    const locked = Boolean(guideMeta.locked);
    await dialogs.showLinkedGuideDialog({
      guide: guideMeta,
      lock: locked ? { acquired: false } : { acquired: true },
      onSave: async () => {
        const result = await api.archive.saveLinked({ guideId: this.guideId, force: false });
        if (result.saved) this.onToast('Linked archive saved.');
        else this.onToast('Could not save linked archive.', { error: true });
      },
      onForceSave: async () => {
        const result = await api.archive.saveLinked({ guideId: this.guideId, force: true });
        if (result.saved) this.onToast('Linked archive force-saved.');
        else this.onToast('Could not save linked archive.', { error: true });
      },
      onOpenArchive: async () => {
        await api.shell.showItemInFolder({ target: this.guide.linkedSource.path });
      },
    });
  }

  async openQuickActions() {
    const commands = [
      { kind: 'cmd', label: 'New guide', description: 'Create a blank guide', action: () => this.onBack('new') },
      { kind: 'cmd', label: 'Export', description: 'Export the current guide', action: () => this.openExportDialog() },
      { kind: 'cmd', label: 'Settings', description: 'Open application settings', action: () => this.openSettings() },
      { kind: 'cmd', label: 'Linked guide', description: 'Show linked archive details', action: () => this.openLinkedGuide() },
      { kind: 'cmd', label: 'Start capture session', description: 'Enable hotkey capture for this guide', action: () => this.startCaptureSession() },
    ];

    await dialogs.showQuickActions({
      commands,
      searchFn: async (query) => {
        const results = await api.search.query({ q: query });
        return results.map((r) => ({
          kind: r.stepId ? 'step' : 'guide',
          label: r.title || '(untitled)',
          description: r.snippet || '',
          action: () => this.openSearchResult(r),
        }));
      },
    });
  }

  async openSearchResult(result) {
    if (!result) return;
    if (result.stepId) {
      await this.onBack();
      await this.open(result.guideId, result.stepId);
    } else {
      await this.onBack();
      await this.open(result.guideId, null);
    }
  }

  async currentStepImageToBase64(step = this.currentStep) {
    if (!step || !step.image) return null;
    const file = await api.step.imagePath({ guideId: this.guideId, stepId: step.stepId, which: 'working' });
    if (!file) return null;
    return new Promise((resolve) => {
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        canvas.width = img.naturalWidth || img.width;
        canvas.height = img.naturalHeight || img.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        const data = canvas.toDataURL('image/png').split(',')[1];
        resolve({ base64: data, size: { width: canvas.width, height: canvas.height } });
      };
      img.onerror = () => resolve(null);
      img.src = file;
    });
  }

  async onCanvasChange(annotations) {
    const step = this.currentStep;
    if (!step) return;
    if (this.beforeCanvasSnapshot) {
      this.canvasHistory.push(this.beforeCanvasSnapshot);
      if (this.canvasHistory.length > 40) this.canvasHistory.shift();
      this.canvasFuture.length = 0;
      this.beforeCanvasSnapshot = null;
    }
    step.annotations = clone(annotations || []);
    this.pendingSave = true;
    this.saveStepDebounced();
    this.renderAnnotationPanel();
    this.renderStepList();
    this.emitMeta();
  }

  onCanvasSelect(ann) {
    this.selectedAnnotationId = ann ? ann.id : null;
    this.renderAnnotationPanel();
    this.emitMeta();
  }

  async onCanvasCrop(rect) {
    const step = this.currentStep;
    if (!step || !step.image) return;
    const ok = await confirmDialog('Crop the working image to the selected area?');
    if (!ok) return;
    this.saveStepDebounced.cancel();
    const snapshot = this.beforeCanvasSnapshot || await this.snapshotCurrentStep(true);
    if (snapshot) {
      if (!snapshot.image) snapshot.image = await this.currentStepImageToBase64(step);
      this.pushCanvasHistory(snapshot);
    }
    this.beforeCanvasSnapshot = null;
    const src = await api.step.imagePath({ guideId: this.guideId, stepId: step.stepId, which: 'working' });
    if (!src) return;
    const img = await loadImage(src);
    if (!img) return;
    const crop = rect;
    const canvas = document.createElement('canvas');
    canvas.width = Math.max(1, Math.round(crop.w * img.naturalWidth));
    canvas.height = Math.max(1, Math.round(crop.h * img.naturalHeight));
    const ctx = canvas.getContext('2d');
    ctx.drawImage(
      img,
      Math.round(crop.x * img.naturalWidth),
      Math.round(crop.y * img.naturalHeight),
      Math.round(crop.w * img.naturalWidth),
      Math.round(crop.h * img.naturalHeight),
      0,
      0,
      canvas.width,
      canvas.height,
    );
    const nextAnnotations = (step.annotations || []).map((ann) => {
      const next = clone(ann);
      next.x = (ann.x - crop.x) / crop.w;
      next.y = (ann.y - crop.y) / crop.h;
      next.w = ann.w / crop.w;
      next.h = ann.h / crop.h;
      return next;
    });
    await api.step.setWorkingImage({
      guideId: this.guideId,
      stepId: step.stepId,
      pngBase64: canvas.toDataURL('image/png').split(',')[1],
      size: { width: canvas.width, height: canvas.height },
    });
    step.image.size = { width: canvas.width, height: canvas.height };
    step.annotations = nextAnnotations;
    await this.flushStep(step);
    await this.reload(step.stepId);
    this.onToast('Image cropped.');
  }

  async editAnnotationText(ann) {
    const step = this.currentStep;
    if (!step || !ann) return;
    const value = await dialogs.promptText({
      title: ann.type === 'tooltip' ? 'Edit tooltip' : 'Edit text',
      label: 'Text',
      value: ann.text || '',
      multiline: true,
    });
    if (value == null) return;
    ann.text = value;
    step.annotations = clone(step.annotations || []);
    this.pendingSave = true;
    await this.flushStep(step);
    this.renderAnnotationPanel();
    this.emitMeta();
  }

  formatDescription(command, block = null) {
    const editor = this.dom.descEditor;
    editor.focus();
    switch (command) {
      case 'bold':
        document.execCommand('bold');
        break;
      case 'italic':
        document.execCommand('italic');
        break;
      case 'insertUnorderedList':
        document.execCommand('insertUnorderedList');
        break;
      case 'insertOrderedList':
        document.execCommand('insertOrderedList');
        break;
      case 'formatBlock':
        document.execCommand('formatBlock', false, block || 'blockquote');
        break;
      case 'createLink': {
        const url = window.prompt('Link URL');
        if (url) document.execCommand('createLink', false, url);
        break;
      }
      case 'removeFormat':
        document.execCommand('removeFormat');
        break;
      default:
        break;
    }
    if (this.currentStep) {
      this.currentStep.descriptionHtml = editor.innerHTML;
      this.pendingSave = true;
      this.saveStepDebounced();
    }
  }

  onDocumentKeyDown(e) {
    if (!this.active || !this.guide) return;
    if ((e.ctrlKey || e.metaKey) && e.key === '/' && !e.shiftKey) {
      e.preventDefault();
      this.openQuickActions();
      return;
    }
    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
      e.preventDefault();
      this.saveAll();
      return;
    }
    if (e.key === 'Escape' && !isEditableTarget(e.target)) {
      // Escape deselects; Delete is the destructive key.
      if (this.selectedAnnotationId) {
        e.preventDefault();
        this.canvas.select(null);
        return;
      }
    }
    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z' && !isEditableTarget(e.target)) {
      e.preventDefault();
      if (e.shiftKey) this.redo();
      else this.undo();
      return;
    }
    if (!isEditableTarget(e.target)) {
      // Tool palette hotkeys (Folge-style single keys).
      const TOOL_KEYS = {
        s: 'select', r: 'rect', o: 'oval', l: 'line', a: 'arrow', t: 'text',
        g: 'tooltip', n: 'number', b: 'blur', h: 'highlight', m: 'magnify',
        u: 'cursor', c: 'crop',
      };
      if (!e.ctrlKey && !e.metaKey && !e.altKey && TOOL_KEYS[e.key.toLowerCase()]) {
        e.preventDefault();
        this.setTool(TOOL_KEYS[e.key.toLowerCase()]);
        return;
      }
      if (e.key === 'PageUp' || e.key === 'PageDown') {
        e.preventDefault();
        const idx = this.steps.findIndex((s) => s.stepId === this.selectedStepId);
        const next = this.steps[idx + (e.key === 'PageDown' ? 1 : -1)];
        if (next) this.selectStep(next.stepId);
        return;
      }
      if ((e.ctrlKey || e.metaKey) && (e.key === '=' || e.key === '+')) {
        e.preventDefault();
        this.setZoom(Math.min(3, (Number(this.currentZoom) || 1) + 0.25));
        return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key === '-') {
        e.preventDefault();
        this.setZoom(Math.max(0.25, (Number(this.currentZoom) || 1) - 0.25));
        return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key === '0') {
        e.preventDefault();
        this.setZoom('fit');
        return;
      }
      // Copy / paste the selected annotation.
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c' && this.selectedAnnotationId) {
        e.preventDefault();
        this.annotationClipboard = clone(this.canvas.selected());
        this.onToast('Annotation copied.');
        return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
        e.preventDefault();
        if (this.annotationClipboard && this.currentStep?.image) {
          const copy = clone(this.annotationClipboard);
          copy.id = `ann-${Date.now().toString(36)}`;
          copy.x = Math.min(0.92, copy.x + 0.03);
          copy.y = Math.min(0.92, copy.y + 0.03);
          this.currentStep.annotations.push(copy);
          this.canvas.setAnnotations(this.currentStep.annotations);
          this.canvas.select(copy.id);
          this.pendingSave = true;
          this.saveStepDebounced();
        } else {
          this.pasteClipboardStep(); // OS clipboard image -> new step
        }
        return;
      }
      if (e.key === 'Delete' && this.selectedAnnotationId) {
        e.preventDefault();
        if (this.canvas.deleteSelected()) this.saveStepDebounced();
        return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key === 'Delete') {
        e.preventDefault();
        this.deleteSelectedStep();
        return;
      }
      if (e.key === 'ArrowUp' && e.altKey) {
        e.preventDefault();
        this.moveSelectedStep(-1);
        return;
      }
      if (e.key === 'ArrowDown' && e.altKey) {
        e.preventDefault();
        this.moveSelectedStep(1);
        return;
      }
      if (e.key.startsWith('Arrow')) {
        const speed = e.shiftKey ? 10 : 1; // shift nudges faster
        const dx = (e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0) * speed;
        const dy = (e.key === 'ArrowUp' ? -1 : e.key === 'ArrowDown' ? 1 : 0) * speed;
        if (dx || dy) {
          const moved = this.canvas.nudgeSelected(dx, dy);
          if (moved) {
            const step = this.currentStep;
            if (step) {
              step.annotations = clone(this.canvas.annotations || []);
              this.pendingSave = true;
              this.saveStepDebounced();
            }
            e.preventDefault();
          }
        }
      }
    }
  }
}

function labeledRow(labelText, control) {
  return el('div.form-row', {}, el('label', {}, labelText), control);
}

function makeSelect(value, options) {
  return el('select', {}, options.map((opt) => el('option', { value: opt.value, selected: opt.value === value }, opt.label)));
}

function loadImage(src) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => resolve(null);
    img.src = src;
  });
}

window.GuideEditor = GuideEditor;
})();
