/*
 * wizard.js - CloudHop setup wizard
 *
 * 6-step wizard flow
 *   Step 1: Welcome / rclone check
 *   Step 2: Select source provider
 *   Step 3: Select destination provider
 *   Step 4: Speed / advanced options
 *   Step 5: Connect accounts (OAuth or credentials)
 *   Step 6: Summary + Start
 *   goTo(step) handles all step transitions; it runs pre-step hooks
 *   (e.g. buildConnectStep, buildSummary) before showing the new step.
 *
 * State persistence
 *   The wizard state (current step, provider names, remote names) is
 *   serialised to sessionStorage on every goTo() call.  An IIFE at the
 *   bottom of the file restores it on load so a page refresh doesn't reset
 *   the user's progress.
 *
 * OAuth flow (Google Drive, OneDrive, Dropbox)
 *   connectRemote() POSTs to /api/wizard/configure-remote which runs
 *   `rclone config create ...`.  For OAuth providers, rclone opens a browser
 *   tab for the sign-in flow.  Meanwhile, startPolling() polls
 *   /api/wizard/check-remote every 2 s to detect when rclone writes the token
 *   to its config file.  The poll times out after 2 minutes and shows a
 *   "Try again" link.
 *
 * Provider keys
 *   `providerKeys` maps the UI provider identifier (e.g. "drive") to the
 *   rclone backend type string (e.g. "gdrive") that is passed to
 *   `rclone config create <name> <type>`.  The remote name used in rclone
 *   paths (e.g. "gdrive:Photos") is stored in sourceName / destName.
 */
// Drag & drop handler for local folder paths
function handleDrop(event, inputId) {
  const items = event.dataTransfer.items || event.dataTransfer.files;
  if (!items || items.length === 0) return;
  const item = items[0];
  if (item.kind !== 'file') return;
  const file = item.getAsFile ? item.getAsFile() : item;
  const input = document.getElementById(inputId);
  if (!input) return;
  let path = file.path || '';
  if (path) {
    // Detect if a file (not a folder) was dropped - extract parent directory
    if (file.type || (file.size > 0 && /\.[^/.]+$/.test((path.split('/').pop() || '')))) {
      const lastSlash = path.lastIndexOf('/');
      if (lastSlash > 0) path = path.substring(0, lastSlash);
    }
    input.value = path;
    input.dispatchEvent(new Event('input', {bubbles: true}));
    // Directly enable Next button (belt-and-suspenders, not relying on event alone)
    if (inputId === 'sourcePathInput') {
      document.getElementById('sourceNext').disabled = !path;
    } else if (inputId === 'destPathInput') {
      document.getElementById('destNext').disabled = !path;
    }
  } else {
    // Regular browser - can't get full path from drag & drop
    const name = file.name || '';
    if (!name) return;
    const hintId = inputId === 'sourcePathInput' ? 'sourcePathError' : 'destPathError';
    const el = document.getElementById(hintId);
    // Show helpful hint (not an error) for both files and folders
    input.placeholder = 'Type the full path to "' + name + '"';
    if (el) {
      el.textContent = 'Drag & drop in browser can\'t detect full paths. Please type the path or use Browse below.';
      el.style.color = 'var(--text-dim)';
      el.style.display = 'block';
    }
    input.focus();
  }
}

window.onerror = function(msg) {
  const el = document.querySelector('.wizard-subtitle');
  if (el && !document.getElementById('_jsErr')) {
    const d = document.createElement('div');
    d.id = '_jsErr';
    d.style.cssText = 'margin-top:12px;padding:10px 16px;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius:10px;font-size:0.75rem;color:var(--orange);text-align:center;';
    d.textContent = 'Something went wrong. Try refreshing the page.';
    el.parentNode.insertBefore(d, el.nextSibling);
  }
  return true;
};

function getCsrfToken(){return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('csrf_token='))?.substring('csrf_token='.length)||''}
function esc(s) {
  const d = document.createElement('div');
  d.textContent = s;
  return d.innerHTML;
}
function isSafeUrl(url) {
  try {
    const u = new URL(url);
    return u.protocol === 'https:';
  } catch { return false; }
}
// State
let currentStep = 1;
let sourceProvider = null;
let sourceName = '';
let sourceDisplayName = '';
let destProvider = null;
let destName = '';
let destDisplayName = '';
let selectedSpeed = '8';
let selectedMode = 'copy';
let maxReachedStep = 1;
let existingRemotes = [];

// Multi-select state
let multiSelectedPaths = [];  // [{path, name, isFolder}]

// Multi-destination state
let multiDestinations = [];  // [{provider, name, displayName, path, subfolder}]
const MAX_DESTINATIONS = 5;

const providerKeys = {
  drive: 'gdrive', onedrive: 'onedrive', dropbox: 'dropbox', mega: 'mega', s3: 's3', protondrive: 'protondrive', local: 'local', icloud: 'local', other: null
};
const providerIcons = {
  drive: '<svg width="32" height="29" viewBox="0 0 87.3 78"><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44c-.8 1.4-1.2 2.9-1.2 4.5h27.5z" fill="#00ac47"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.5l5.85 11.5z" fill="#ea4335"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/></svg>',
  onedrive: '<svg width="32" height="22" viewBox="0 0 24 16"><defs><linearGradient id="odgjs" x1="0" y1="0" x2="24" y2="16" gradientUnits="userSpaceOnUse"><stop stop-color="#0364b8"/><stop offset="1" stop-color="#28a8ea"/></linearGradient></defs><path d="M19.5 7c-.2 0-.4 0-.6.1C18.1 3.6 15.3 1 12 1 9.2 1 6.8 2.8 5.7 5.4 5.5 5.3 5.2 5.2 5 5.2 3.3 5.2 2 6.5 2 8.2c0 .2 0 .4.1.6C.8 9.4 0 10.8 0 12.5 0 15 2 17 4.5 17H19c2.8 0 5-2.2 5-5s-2.2-5-5-5z" fill="url(#odgjs)"/></svg>',
  dropbox: '<svg width="28" height="28" viewBox="0 0 16 16"><path d="M8.01 4.555 4.005 7.11 8.01 9.665 4.005 12.22 0 9.651l4.005-2.555L0 4.555 4.005 2zm-4.026 8.487 4.006-2.555 4.005 2.555-4.005 2.555zm4.026-3.39 4.005-2.556L8.01 4.555 11.995 2 16 4.555 11.995 7.11 16 9.665l-4.005 2.555z" fill="#0061fe"/></svg>',
  mega: '<svg width="28" height="28" viewBox="100 141 118 118"><path d="m158.711 141.289c-32.426 0-58.711 26.285-58.711 58.711s26.285 58.711 58.711 58.711 58.711-26.285 58.711-58.711-26.285-58.711-58.711-58.711zm30.476 79.473c0 1.007-.812 1.819-1.819 1.819h-7.668c-1.007 0-1.819-.812-1.819-1.819v-23.621c0-.195-.228-.293-.39-.163l-16.246 16.246c-1.397 1.397-3.704 1.397-5.101 0l-16.245-16.246c-.13-.13-.39-.032-.39.163v23.621c0 1.007-.812 1.819-1.82 1.819h-7.667c-1.008 0-1.82-.812-1.82-1.819v-41.524c0-1.007.812-1.819 1.82-1.819h5.263c.942 0 1.885.39 2.567 1.072l20.209 20.209c.358.358.91.358 1.267 0l20.21-20.209c.682-.682 1.592-1.072 2.566-1.072h5.264c1.007 0 1.819.812 1.819 1.819z" fill="#d9272e"/></svg>',
  s3: '<svg width="28" height="28" viewBox="0 0 24 24"><path d="M20 4H4l-1 8h18l-1-8z" fill="#f90"/><path d="M3 12l1 8h16l1-8H3z" fill="#f90" opacity=".8"/><ellipse cx="12" cy="4" rx="8" ry="2" fill="#f90" opacity=".6"/><text x="12" y="16" font-family="Arial,sans-serif" font-size="7" font-weight="bold" fill="white" text-anchor="middle">S3</text></svg>',
  protondrive: '<svg width="28" height="26" viewBox="0 0 24 22"><defs><linearGradient id="pdgjs" x1="0" y1="0" x2="24" y2="22" gradientUnits="userSpaceOnUse"><stop stop-color="#d4b4f7"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs><path d="M20 6H4C2.9 6 2 6.9 2 8v10c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z" fill="url(#pdgjs)"/><path d="M22 5H10L8 2H4c-1.1 0-2 .9-2 2v1h16c1.1 0 2 .9 2 2V5z" fill="url(#pdgjs)" opacity=".6"/></svg>',
  local: '<svg width="28" height="28" viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z" fill="var(--text-dim)"/></svg>',
  icloud: '<svg width="32" height="22" viewBox="0 0 24 16"><defs><linearGradient id="icgjs" x1="0" y1="0" x2="24" y2="16" gradientUnits="userSpaceOnUse"><stop stop-color="#a8d8f0"/><stop offset="1" stop-color="#5ab0e4"/></linearGradient></defs><path d="M19 7c-.2 0-.3 0-.5.1C17.7 3.5 15 1 12 1 9.2 1 6.8 2.8 5.7 5.4 5.5 5.3 5.3 5.3 5 5.3 3.3 5.3 2 6.6 2 8.3c0 .2 0 .4.1.5C.8 9.5 0 10.9 0 12.5 0 14.9 2 17 4.5 17h14c2.8 0 5-2.2 5-5s-2.2-5-4.5-5z" fill="url(#icgjs)"/></svg>',
  other: '<svg width="28" height="28" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1112 8.4a3.6 3.6 0 010 7.2z" fill="var(--text-dim)"/></svg>'
};

// Theme
function _updateThemeIcons(isLight) {
  const dark = document.getElementById('theme-icon-dark');
  const light = document.getElementById('theme-icon-light');
  if (dark) dark.style.display = isLight ? 'none' : 'block';
  if (light) light.style.display = isLight ? 'block' : 'none';
}
function toggleTheme() {
  const html = document.documentElement;
  const current = html.getAttribute('data-theme');
  const next = current === 'light' ? 'dark' : 'light';
  document.body.style.transition = 'none';
  html.setAttribute('data-theme', next);
  _updateThemeIcons(next === 'light');
  localStorage.setItem('cloudhop-theme', next);
  requestAnimationFrame(() => { requestAnimationFrame(() => { document.body.style.transition = ''; }); });
}
(function() {
  const saved = localStorage.getItem('cloudhop-theme');
  if (!saved && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
    document.documentElement.setAttribute('data-theme', 'light');
    _updateThemeIcons(true);
  }
  if (saved === 'light') {
    document.documentElement.setAttribute('data-theme', 'light');
    _updateThemeIcons(true);
  }
  window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
    if (!localStorage.getItem('cloudhop-theme')) {
      document.documentElement.setAttribute('data-theme', e.matches ? 'light' : 'dark');
      _updateThemeIcons(e.matches);
    }
  });
})();

// Fetch home directory and check rclone on page load
(function() {
  fetch('/api/wizard/status').then(r => r.json()).then(d => {
    if (d.home_dir) window._homeDir = d.home_dir;
    const el = document.getElementById('welcomeRcloneCheck');
    if (!d.rclone_installed) {
      el.innerHTML = '<span style="color:var(--orange)">Required components will be installed automatically when you proceed.</span>';
    }
  }).catch(() => {});
  // Check for updates on wizard load
  fetch('/api/check-update').then(r => r.json()).then(d => {
    if (d.update_available) {
      const el = document.getElementById('welcomeRcloneCheck');
      if (el) {
        el.innerHTML = '';
        const span = document.createElement('span');
        span.style.color = 'var(--blue)';
        span.textContent = 'Update available: v' + d.latest + ' ';
        if (d.download_url && isSafeUrl(d.download_url)) {
          const a = document.createElement('a');
          a.href = d.download_url;
          a.target = '_blank';
          a.style.cssText = 'color:var(--blue);text-decoration:underline;';
          a.textContent = 'Download ' + d.latest;
          span.appendChild(a);
        } else if (d.pip_command) {
          if (d.download_url) {
            console.error('CloudHop: update download URL rejected by validation:', d.download_url);
          }
          const code = document.createElement('code');
          code.style.cssText = 'background:var(--card);padding:2px 6px;border-radius:4px;';
          code.textContent = d.pip_command;
          span.appendChild(code);
        } else if (d.download_url) {
          console.error('CloudHop: update download URL rejected by validation:', d.download_url);
        }
        el.appendChild(span);
      }
    }
  }).catch(() => {});
})();

// Styled confirm modal (replaces native confirm())
function showConfirmModal(message) {
  return new Promise((resolve) => {
    if (document.getElementById('_cm_wiz_overlay')) return resolve(false);
    const overlay = document.createElement('div');
    overlay.id = '_cm_wiz_overlay';
    overlay.setAttribute('role', 'dialog');
    overlay.setAttribute('aria-modal', 'true');
    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:400;display:flex;align-items:center;justify-content:center;';
    const box = document.createElement('div');
    box.style.cssText = 'background:var(--card);border:1px solid var(--card-border);border-radius:16px;padding:28px 24px;max-width:440px;width:90%;text-align:center;';
    box.innerHTML = '<div style="font-size:0.95rem;color:var(--text);margin-bottom:20px;line-height:1.6;">' + esc(message) + '</div>'
      + '<div style="display:flex;gap:12px;justify-content:center;">'
      + '<button id="cmConfirmCancel" class="btn btn-secondary" style="padding:10px 24px;border-radius:10px;font-size:0.85rem;cursor:pointer;">Cancel</button>'
      + '<button id="cmConfirmOk" class="btn btn-primary" style="padding:10px 24px;border-radius:10px;font-size:0.85rem;cursor:pointer;">Continue</button>'
      + '</div>';
    overlay.appendChild(box);
    document.body.appendChild(overlay);
    function cleanup() { overlay.remove(); document.removeEventListener('keydown', escHandler); }
    function escHandler(e) { if (e.key === 'Escape') { cleanup(); resolve(false); } }
    document.addEventListener('keydown', escHandler);
    box.querySelector('#cmConfirmCancel').onclick = () => { cleanup(); resolve(false); };
    box.querySelector('#cmConfirmOk').onclick = () => { cleanup(); resolve(true); };
    overlay.addEventListener('click', (e) => { if (e.target === overlay) { cleanup(); resolve(false); } });
    box.querySelector('#cmConfirmOk').focus();
  });
}

// F506: Mirror mode confirmation dialog
function showMirrorConfirm() {
  return new Promise((resolve) => {
    if (document.getElementById('_mirror_overlay')) return resolve(false);
    const overlay = document.createElement('div');
    overlay.id = '_mirror_overlay';
    overlay.setAttribute('role', 'dialog');
    overlay.setAttribute('aria-modal', 'true');
    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:400;display:flex;align-items:center;justify-content:center;';
    const box = document.createElement('div');
    box.style.cssText = 'background:var(--card);border:2px solid rgba(248,113,113,0.4);border-radius:16px;padding:28px 24px;max-width:440px;width:90%;text-align:center;';
    box.innerHTML = '<div style="font-size:1.1rem;font-weight:700;color:var(--red);margin-bottom:12px;">&#9888; Mirror Mode</div>'
      + '<div style="font-size:0.9rem;color:var(--text);margin-bottom:16px;line-height:1.6;">Mirror mode will <strong>DELETE</strong> files from destination that don\'t exist in source.<br>Type <strong>MIRROR</strong> to confirm.</div>'
      + '<input id="_mirrorInput" type="text" autocomplete="off" placeholder="Type MIRROR" style="width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--card-border);background:var(--bg);color:var(--text);font-size:1rem;text-align:center;margin-bottom:16px;box-sizing:border-box;">'
      + '<div style="display:flex;gap:12px;justify-content:center;">'
      + '<button id="_mirrorCancel" class="btn btn-secondary" style="padding:10px 24px;border-radius:10px;font-size:0.85rem;cursor:pointer;">Cancel</button>'
      + '<button id="_mirrorOk" class="btn btn-primary" style="padding:10px 24px;border-radius:10px;font-size:0.85rem;cursor:pointer;opacity:0.5;pointer-events:none;">Confirm</button>'
      + '</div>';
    overlay.appendChild(box);
    document.body.appendChild(overlay);
    const input = box.querySelector('#_mirrorInput');
    const okBtn = box.querySelector('#_mirrorOk');
    input.addEventListener('input', function() {
      const match = this.value.trim().toUpperCase() === 'MIRROR';
      okBtn.style.opacity = match ? '1' : '0.5';
      okBtn.style.pointerEvents = match ? 'auto' : 'none';
    });
    function cleanup() { overlay.remove(); document.removeEventListener('keydown', escHandler); }
    function escHandler(e) { if (e.key === 'Escape') { cleanup(); resolve(false); } }
    document.addEventListener('keydown', escHandler);
    box.querySelector('#_mirrorCancel').onclick = () => { cleanup(); resolve(false); };
    okBtn.onclick = () => {
      if (input.value.trim().toUpperCase() === 'MIRROR') {
        console.log('[F506] Sync confirmation required, user typed:', input.value);
        cleanup();
        resolve(true);
      }
    };
    overlay.addEventListener('click', (e) => { if (e.target === overlay) { cleanup(); resolve(false); } });
    input.focus();
  });
}

function showStepError(msg) {
  const existing = document.getElementById('_stepError');
  if (existing) existing.remove();
  const d = document.createElement('div');
  d.id = '_stepError';
  d.style.cssText = 'position:fixed;top:24px;left:50%;transform:translateX(-50%);padding:10px 20px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:10px;font-size:0.85rem;color:var(--red);z-index:300;text-align:center;';
  d.textContent = msg;
  const dismiss = document.createElement('span');
  dismiss.textContent = '\u00d7';
  dismiss.style.cssText = 'margin-left:12px;cursor:pointer;font-size:1.1rem;font-weight:700;opacity:0.7;';
  dismiss.onclick = () => d.remove();
  d.appendChild(dismiss);
  document.body.appendChild(d);
  setTimeout(() => { if (d.parentNode) d.remove(); }, 7000);
}

// Navigation
async function goTo(step) {
  // F701: Prevent skipping steps via console
  if (step > maxReachedStep + 1) {
    console.log('[F701] goTo validation: requested=%d, max_allowed=%d', step, maxReachedStep + 1);
    return;
  }
  if (step >= 3 && !sourceProvider) {
    showStepError('Please select a source provider to continue');
    return;
  }
  if (step >= 4 && !destProvider) {
    showStepError('Please select a destination provider to continue');
    return;
  }
  // Validate local source path early (Bug 3)
  if (step === 3 && (sourceProvider === 'local' || sourceProvider === 'icloud')) {
    const pathInput = document.getElementById('sourcePathInput');
    const path = pathInput ? pathInput.value.trim() : '';
    if (path) {
      // F703: Truncate overly long paths
      if (path.length > 500) {
        pathInput.value = path.substring(0, 500);
        console.log('[F703] Path truncated to max length');
      }
      // F704: Reject path traversal
      if (path.includes('..')) {
        const errEl = document.getElementById('sourcePathError');
        if (errEl) { errEl.textContent = 'Path cannot contain ".." for security reasons.'; errEl.style.display = 'block'; }
        console.log('[F704] Path traversal rejected client-side');
        return;
      }
      try {
        const vResp = await fetch('/api/wizard/validate-path', {
          method: 'POST',
          headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
          body: JSON.stringify({path: path})
        });
        const vData = await vResp.json();
        if (!vData.exists) {
          const errEl = document.getElementById('sourcePathError');
          if (errEl) { errEl.textContent = 'This path does not exist. Please check and try again.'; errEl.style.display = 'block'; }
          return;
        }
        if (!vData.is_directory) {
          const errEl = document.getElementById('sourcePathError');
          if (errEl) { errEl.textContent = 'This path is not a directory. Please select a folder.'; errEl.style.display = 'block'; }
          return;
        }
        document.getElementById('sourcePathError').style.display = 'none';
      } catch(e) {
        // If validation endpoint unavailable, let it proceed
      }
    }
  }
  if (step === 3) updateDestGrid();
  if (step === 5) buildConnectStep();
  if (step === 6) {
    if (sourceProvider === destProvider && sourceProvider !== 'local' && sourceProvider !== 'icloud') {
      const proceed = await showConfirmModal('Source and destination are the same service. Two separate accounts will be set up (e.g. "gdrive" for source and "gdrive_dest" for destination). Continue?');
      if (!proceed) return;
    }
    buildSummary();
  }

  document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
  document.getElementById('step' + step).classList.add('active');

  const dots = document.querySelectorAll('.dot');
  dots.forEach((d, i) => {
    d.classList.remove('active', 'done');
    if (i < step - 1) d.classList.add('done');
    if (i === step - 1) d.classList.add('active');
  });

  // Restore previous selections when navigating back
  if (step === 2 && sourceProvider) {
    const sc = document.querySelector('#sourceGrid [data-provider="'+sourceProvider+'"]');
    if (sc && !sc.classList.contains('selected')) {
      document.querySelectorAll('#sourceGrid .provider-card').forEach(c => c.classList.remove('selected'));
      sc.classList.add('selected');
    }
    // F321: Restore tab order for source providers
    if (sc) _updateProviderTabOrder('sourceGrid', sc);
    document.getElementById('sourceLocalPath').classList.toggle('show', sourceProvider === 'local' || sourceProvider === 'icloud');
    document.getElementById('sourceOtherName').classList.toggle('show', sourceProvider === 'other');
    if (sourceProvider === 'local' || sourceProvider === 'icloud') {
      document.getElementById('sourceNext').disabled = !document.getElementById('sourcePathInput').value.trim();
    } else if (sourceProvider === 'other') {
      document.getElementById('sourceNext').disabled = !document.getElementById('sourceOtherInput').value.trim();
    } else {
      document.getElementById('sourceNext').disabled = false;
    }
  }
  if (step === 3 && destProvider) {
    const dc = document.querySelector('#destGrid [data-provider="'+destProvider+'"]');
    if (dc && !dc.classList.contains('selected')) {
      document.querySelectorAll('#destGrid .provider-card').forEach(c => c.classList.remove('selected'));
      dc.classList.add('selected');
    }
    // F321: Restore tab order for dest providers
    if (dc) _updateProviderTabOrder('destGrid', dc);
    document.getElementById('destLocalPath').classList.toggle('show', destProvider === 'local' || destProvider === 'icloud');
    document.getElementById('destOtherName').classList.toggle('show', destProvider === 'other');
    if (destProvider === 'local' || destProvider === 'icloud') {
      document.getElementById('destNext').disabled = !document.getElementById('destPathInput').value.trim();
    } else if (destProvider === 'other') {
      document.getElementById('destNext').disabled = !document.getElementById('destOtherInput').value.trim();
    } else {
      document.getElementById('destNext').disabled = false;
    }
  }
  if (step === 4) {
    // Restore parallel transfers select
    if (selectedSpeed) {
      const ptEl = document.getElementById('parallelTransfers');
      if (ptEl) ptEl.value = selectedSpeed;
    }
    // Hide source subfolder when local path already specifies a subfolder
    const srcPath = (document.getElementById('sourcePathInput') || {}).value || '';
    const isLocalDetailed = (sourceProvider === 'local' || sourceProvider === 'icloud') && srcPath.includes('/');
    const sfGroup = document.getElementById('sourceSubfolder');
    if (sfGroup) sfGroup.closest('.form-group').style.display = isLocalDetailed ? 'none' : '';
    // Restore mode card selection
    document.querySelectorAll('.mode-card').forEach(c => {
      c.classList.remove('selected');
      c.setAttribute('aria-checked', 'false');
    });
    document.querySelectorAll('.mode-card').forEach(c => {
      const radio = c.querySelector('input[name="mode"]');
      if (radio && radio.value === selectedMode) {
        c.classList.add('selected');
        c.setAttribute('aria-checked', 'true');
        radio.checked = true;
      }
    });
    var mw = document.getElementById('modeWarning');
    if (mw) mw.style.display = selectedMode === 'sync' ? 'block' : 'none';
    // F321: Ensure collapsed advanced options are out of tab order
    _updateAdvancedTabOrder();
  }

  currentStep = step;
  if (step > maxReachedStep) maxReachedStep = step;
  // Save wizard state to survive page refresh
  try {
    sessionStorage.setItem('cloudhop_wizard', JSON.stringify({
      step: currentStep, maxReachedStep, sourceProvider, sourceName, sourceDisplayName,
      destProvider, destName, destDisplayName, selectedSpeed, selectedMode,
      sourcePath: (document.getElementById('sourcePathInput') || {}).value || '',
      destPath: (document.getElementById('destPathInput') || {}).value || '',
      sourceOther: (document.getElementById('sourceOtherInput') || {}).value || '',
      destOther: (document.getElementById('destOtherInput') || {}).value || '',
      sourceSubfolder: (document.getElementById('sourceSubfolder') || {}).value || '',
      destSubfolder: (document.getElementById('destSubfolder') || {}).value || '',
      excludePatterns: (document.getElementById('excludePatterns') || {}).value || '',
      bwLimit: (document.getElementById('bwLimit') || {}).value || '',
      useChecksum: (document.getElementById('useChecksum') || {}).checked || false,
      useFastList: (document.getElementById('useFastList') || {}).checked || false
    }));
  } catch(e) {}
}

function toggleAdvanced() {
    const content = document.getElementById('advancedContent');
    const arrow = document.getElementById('advArrow');
    content.classList.toggle('open');
    arrow.classList.toggle('open');
    // F321: Update tab order for advanced options based on visibility
    _updateAdvancedTabOrder();
}

// Schedule toggle
(function() {
  document.addEventListener('DOMContentLoaded', function() {
    const schedEl = document.getElementById('scheduleEnabled');
    if (schedEl) {
      schedEl.addEventListener('change', function() {
        document.getElementById('scheduleConfig').style.display = this.checked ? 'block' : 'none';
      });
    }
  });
})();

// Restore wizard state after refresh
(function() {
  try {
    // F309: If navigated via "New Transfer", clear all wizard state and start fresh
    const params = new URLSearchParams(window.location.search);
    if (params.get('new') === '1') {
      sessionStorage.removeItem('cloudhop_wizard');
      // Clean up URL without reload
      window.history.replaceState({}, '', window.location.pathname);
      console.log('[F309] Wizard state reset for new transfer');
      return;
    }
    const saved = sessionStorage.getItem('cloudhop_wizard');
    if (!saved) return;
    const s = JSON.parse(saved);
    if (!s.sourceProvider) return;
    maxReachedStep = s.maxReachedStep || s.step || 1;
    sourceProvider = s.sourceProvider;
    sourceName = s.sourceName || '';
    sourceDisplayName = s.sourceDisplayName || '';
    destProvider = s.destProvider;
    destName = s.destName || '';
    destDisplayName = s.destDisplayName || '';
    selectedSpeed = s.selectedSpeed || '8';
    selectedMode = s.selectedMode || 'copy';
    // Re-select cards visually
    if (sourceProvider) {
      const sc = document.querySelector('#sourceGrid [data-provider="'+sourceProvider+'"]');
      if (sc) sc.classList.add('selected');
    }
    if (destProvider) {
      const dc = document.querySelector('#destGrid [data-provider="'+destProvider+'"]');
      if (dc) dc.classList.add('selected');
    }
    // Restore form field values
    if (s.sourcePath) { const el = document.getElementById('sourcePathInput'); if (el) el.value = s.sourcePath; }
    if (s.destPath) { const el = document.getElementById('destPathInput'); if (el) el.value = s.destPath; }
    if (s.sourceOther) { const el = document.getElementById('sourceOtherInput'); if (el) el.value = s.sourceOther; }
    if (s.destOther) { const el = document.getElementById('destOtherInput'); if (el) el.value = s.destOther; }
    if (s.sourceSubfolder) { const el = document.getElementById('sourceSubfolder'); if (el) el.value = s.sourceSubfolder; }
    if (s.destSubfolder) { const el = document.getElementById('destSubfolder'); if (el) el.value = s.destSubfolder; }
    if (s.excludePatterns) { const el = document.getElementById('excludePatterns'); if (el) el.value = s.excludePatterns; }
    if (s.bwLimit) { const el = document.getElementById('bwLimit'); if (el) el.value = s.bwLimit; }
    if (s.useChecksum) { const el = document.getElementById('useChecksum'); if (el) el.checked = true; }
    if (s.useFastList === false) { const el = document.getElementById('useFastList'); if (el) el.checked = false; }
    if (s.selectedSpeed) { const el = document.getElementById('parallelTransfers'); if (el) el.value = s.selectedSpeed; }
    if (s.step > 1) goTo(s.step);
  } catch(e) {}
})();

// F321: Update tab order for a provider grid after selection
function _updateProviderTabOrder(gridId, selectedCard) {
  document.querySelectorAll('#' + gridId + ' .provider-card').forEach(c => {
    if (c === selectedCard) {
      c.setAttribute('tabindex', '0');
    } else {
      c.setAttribute('tabindex', '-1');
    }
  });
  console.log('[F321] Tab order updated: active provider=%s', selectedCard.dataset.provider);
}

// F321: Manage tab order for advanced options based on collapsed/expanded state
function _updateAdvancedTabOrder() {
  const content = document.getElementById('advancedContent');
  if (!content) return;
  const isOpen = content.classList.contains('open');
  content.querySelectorAll('input, select, button, [tabindex]').forEach(el => {
    if (isOpen) {
      el.removeAttribute('tabindex');
    } else {
      el.setAttribute('tabindex', '-1');
    }
  });
}

// Source selection
function selectSource(card) {
  document.querySelectorAll('#sourceGrid .provider-card').forEach(c => c.classList.remove('selected'));
  card.classList.add('selected');
  sourceProvider = card.dataset.provider;
  sourceDisplayName = card.dataset.name;
  sourceName = providerKeys[sourceProvider] || sourceProvider;

  // F321: Remove unselected providers from tab order
  _updateProviderTabOrder('sourceGrid', card);

  // F301: Clear subfolder when source type changes to prevent stale values
  const srcSubEl = document.getElementById('sourceSubfolder');
  if (srcSubEl) { srcSubEl.value = ''; }
  console.log('[F301] Cleared subfolder on source type change');

  document.getElementById('sourceLocalPath').classList.toggle('show', sourceProvider === 'local' || sourceProvider === 'icloud');
  document.getElementById('sourceOtherName').classList.toggle('show', sourceProvider === 'other');
  if (sourceProvider === 'icloud') {
    // Auto-fill with macOS iCloud Drive path
    const pathInput = document.getElementById('sourcePathInput');
    if (pathInput) pathInput.value = (window._homeDir || '') + '/Library/Mobile Documents/com~apple~CloudDocs';
  }
  // For Other provider, only enable Next when name is entered
  if (sourceProvider === 'other') {
    const input = document.getElementById('sourceOtherInput');
    document.getElementById('sourceNext').disabled = !input.value.trim();
    if (!input.dataset.listening) {
      input.dataset.listening = 'true';
      input.addEventListener('input', () => {
        sourceName = input.value.trim();
        document.getElementById('sourceNext').disabled = !input.value.trim();
      });
    }
    // F321: Auto-focus the relevant input
    input.focus();
  } else if (sourceProvider === 'local' || sourceProvider === 'icloud') {
    const input = document.getElementById('sourcePathInput');
    document.getElementById('sourceNext').disabled = !input.value.trim();
    if (!input.dataset.listening) {
      input.dataset.listening = 'true';
      input.addEventListener('input', () => {
        document.getElementById('sourceNext').disabled = !input.value.trim();
      });
    }
    // F321: Auto-focus the path input
    input.focus();
  } else {
    document.getElementById('sourceNext').disabled = false;
    // F321: Auto-focus the Next button for cloud providers (no input needed)
    document.getElementById('sourceNext').focus();
  }
}

// Dest selection
function selectDest(card) {
  if (card.classList.contains('disabled')) return;
  document.querySelectorAll('#destGrid .provider-card').forEach(c => c.classList.remove('selected'));
  card.classList.add('selected');
  destProvider = card.dataset.provider;
  destDisplayName = card.dataset.name;
  destName = providerKeys[destProvider] || destProvider;
  if (destProvider === sourceProvider && sourceProvider !== 'local') {
    destName = sourceName + '_dest';
  }

  // F321: Remove unselected providers from tab order
  _updateProviderTabOrder('destGrid', card);

  document.getElementById('destLocalPath').classList.toggle('show', destProvider === 'local' || destProvider === 'icloud');
  document.getElementById('destOtherName').classList.toggle('show', destProvider === 'other');
  if (destProvider === 'icloud') {
    // Auto-fill with macOS iCloud Drive path
    const pathInput = document.getElementById('destPathInput');
    if (pathInput) pathInput.value = (window._homeDir || '') + '/Library/Mobile Documents/com~apple~CloudDocs';
  }
  // For Other provider, only enable Next when name is entered
  if (destProvider === 'other') {
    const input = document.getElementById('destOtherInput');
    document.getElementById('destNext').disabled = !input.value.trim();
    if (!input.dataset.listening) {
      input.dataset.listening = 'true';
      input.addEventListener('input', () => {
        destName = input.value.trim();
        document.getElementById('destNext').disabled = !input.value.trim();
      });
    }
    // F321: Auto-focus the relevant input
    input.focus();
  } else if (destProvider === 'local' || destProvider === 'icloud') {
    const input = document.getElementById('destPathInput');
    document.getElementById('destNext').disabled = !input.value.trim();
    if (!input.dataset.listening) {
      input.dataset.listening = 'true';
      input.addEventListener('input', () => {
        document.getElementById('destNext').disabled = !input.value.trim();
      });
    }
    // F321: Auto-focus the path input
    input.focus();
  } else {
    document.getElementById('destNext').disabled = false;
    // F321: Auto-focus the Next button for cloud providers
    document.getElementById('destNext').focus();
  }
  // Show "Add another destination" button when a dest is selected
  const addBtn = document.getElementById('addDestBtn');
  if (addBtn && (multiDestinations.length + (destProvider ? 1 : 0)) < MAX_DESTINATIONS) {
    addBtn.style.display = 'inline-flex';
  }
}

function updateDestGrid() {
  document.querySelectorAll('#destGrid .provider-card').forEach(c => {
    c.classList.remove('disabled');
    const note = c.querySelector('.same-provider-note');
    if (note) note.remove();
    if (c.dataset.provider === sourceProvider && sourceProvider !== 'local' && sourceProvider !== 'other') {
      const span = document.createElement('div');
      span.className = 'same-provider-note';
      span.style.cssText = 'font-size:0.75rem;color:var(--text-dim);margin-top:4px;';
      span.textContent = '(will configure as separate account)';
      c.appendChild(span);
    }
  });
}

// ── Multi-destination helpers ────────────────────────────────────────
function addCurrentDestToMulti() {
  if (!destProvider) return;
  if (multiDestinations.length >= MAX_DESTINATIONS) return;
  const sub = (document.getElementById('destSubfolder') || {}).value || '';
  let path;
  if (destProvider === 'local' || destProvider === 'icloud') {
    path = (document.getElementById('destPathInput') || {}).value || '';
  } else if (destProvider === 'other') {
    const n = (document.getElementById('destOtherInput') || {}).value || '';
    path = n + ':' + sub;
  } else {
    path = destName + ':' + sub;
  }
  multiDestinations.push({
    provider: destProvider,
    name: destName,
    displayName: destDisplayName,
    path: path,
    subfolder: sub,
  });
  renderMultiDestList();
}

function removeMultiDest(idx) {
  multiDestinations.splice(idx, 1);
  renderMultiDestList();
  // F307: Re-evaluate Next button - enable if we still have valid destinations or a current selection
  const nextBtn = document.getElementById('destNext');
  if (nextBtn) {
    nextBtn.disabled = !destProvider && multiDestinations.length === 0;
  }
  console.log('[F307] Multi-dest: slot added/removed, valid count:', multiDestinations.length);
}

function renderMultiDestList() {
  const container = document.getElementById('multiDestList');
  if (!container) return;
  if (multiDestinations.length === 0) {
    container.style.display = 'none';
    const btn = document.getElementById('addDestBtn');
    if (btn) btn.style.display = 'none';
    return;
  }
  container.style.display = 'block';
  const btn = document.getElementById('addDestBtn');
  if (btn) btn.style.display = multiDestinations.length < MAX_DESTINATIONS ? 'inline-flex' : 'none';
  let html = '<div style="font-size:0.8rem;color:var(--text-dim);margin-bottom:8px;">' + multiDestinations.length + ' destination' + (multiDestinations.length > 1 ? 's' : '') + ' selected</div>';
  multiDestinations.forEach((d, i) => {
    const icon = providerIcons[d.provider] || providerIcons.other;
    const label = d.displayName + (d.subfolder ? ' / ' + d.subfolder : '');
    html += '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--bg);border:1px solid var(--card-border);border-radius:8px;margin-bottom:6px;">'
      + '<span style="flex-shrink:0;">' + icon + '</span>'
      + '<span style="flex:1;font-size:0.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(label) + '">' + esc(label) + '</span>'
      + '<button type="button" onclick="removeMultiDest(' + i + ')" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:1.1rem;padding:4px 8px;line-height:1;" title="Remove">&times;</button>'
      + '</div>';
  });
  container.innerHTML = html;
}

function onAddAnotherDest() {
  // Save current dest selection into the multi-dest list
  if (!destProvider) return;
  addCurrentDestToMulti();
  // Reset the dest selection UI for a new pick
  document.querySelectorAll('#destGrid .provider-card').forEach(c => c.classList.remove('selected'));
  document.getElementById('destLocalPath').classList.remove('show');
  document.getElementById('destOtherName').classList.remove('show');
  // F307: Keep Next enabled if we already have valid destinations
  document.getElementById('destNext').disabled = multiDestinations.length === 0;
  console.log('[F307] Multi-dest: slot added/removed, valid count:', multiDestinations.length);
  destProvider = null;
  destName = '';
  destDisplayName = '';
  if (document.getElementById('destPathInput')) document.getElementById('destPathInput').value = '';
  if (document.getElementById('destOtherInput')) document.getElementById('destOtherInput').value = '';
  if (document.getElementById('destSubfolder')) document.getElementById('destSubfolder').value = '';
}

function getMultiDestPaths() {
  // Build the final list of destinations for the API
  const dests = multiDestinations.map(d => ({remote: d.provider === 'icloud' ? 'local' : d.provider, path: d.path}));
  // Also include current dest if selected (the "last" one not yet added)
  if (destProvider) {
    const sub = (document.getElementById('destSubfolder') || {}).value || '';
    let path;
    if (destProvider === 'local' || destProvider === 'icloud') {
      path = (document.getElementById('destPathInput') || {}).value || '';
    } else if (destProvider === 'other') {
      const n = (document.getElementById('destOtherInput') || {}).value || '';
      path = n + ':' + sub;
    } else {
      path = destName + ':' + sub;
    }
    dests.push({remote: destProvider === 'icloud' ? 'local' : destProvider, path: path});
  }
  return dests;
}

function isMultiDest() {
  return multiDestinations.length > 0;
}

// selectSpeed removed - now using #parallelTransfers select in Advanced Options

function selectMode(card, val) {
  document.querySelectorAll('.mode-card').forEach(c => { c.classList.remove('selected'); c.setAttribute('aria-checked', 'false'); });
  card.classList.add('selected');
  card.setAttribute('aria-checked', 'true');
  selectedMode = val;
  var w = document.getElementById('modeWarning');
  if (w) w.style.display = val === 'sync' ? 'block' : 'none';
}

// Build connect step
async function buildConnectStep() {
  const list = document.getElementById('connectList');
  list.innerHTML = '';

  // Set hint based on provider types
  const oauthProviders = ['drive','onedrive','dropbox'];
  const hasOAuth = oauthProviders.includes(sourceProvider) || oauthProviders.includes(destProvider);
  const credProviders = ['mega','protondrive','s3'];
  const hasCred = credProviders.includes(sourceProvider) || credProviders.includes(destProvider);
  const hint = document.getElementById('connectHint');
  if (hasOAuth && hasCred) {
    hint.innerHTML = 'Some services will open a browser window for sign-in. Others will ask for your username and password below.';
  } else if (hasOAuth) {
    hint.innerHTML = 'A browser window will open. Sign in to your account there, then come back to this page. CloudHop will detect the connection automatically.';
  } else if (hasCred) {
    hint.innerHTML = 'Enter your credentials below to connect your accounts.';
  } else {
    hint.innerHTML = '';
  }

  // Check rclone first
  const statusEl = document.getElementById('rcloneStatus');
  statusEl.innerHTML = '<div class="spinner"></div> Setting up...';
  try {
    const resp = await fetch('/api/wizard/status');
    const data = await resp.json();
    existingRemotes = data.remotes || [];
    if (data.home_dir) window._homeDir = data.home_dir;
    if (!data.rclone_installed) {
      statusEl.innerHTML = '<div class="spinner" style="display:inline-block;margin-right:8px;vertical-align:middle;"></div><span style="color:var(--orange)">Installing rclone...</span>';
      const installResp = await fetch('/api/wizard/install-rclone', {method:'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()}});
      if (!installResp.ok) {
        const errText = await installResp.text();
        console.error('rclone install HTTP error:', installResp.status, errText);
        statusEl.innerHTML = '<span style="color:var(--red)">Setup failed. <a href="https://rclone.org/install/" target="_blank" style="color:var(--red);text-decoration:underline;">Install rclone manually</a></span>';
        return;
      }
      const installData = await installResp.json();
      if (!installData.ok) {
        console.error('rclone install failed:', installData.msg);
        statusEl.innerHTML = '<span style="color:var(--red)">' + esc(installData.msg || 'Setup failed.') + ' <a href="https://rclone.org/install/" target="_blank" style="color:var(--red);text-decoration:underline;">Install manually</a></span>';
        return;
      }
      statusEl.innerHTML = '<span style="color:var(--green)">Setup complete!</span>';
    } else {
      statusEl.innerHTML = '';
    }
  } catch(e) {
    statusEl.innerHTML = '';
  }

  const items = [];
  if (sourceProvider && sourceProvider !== 'local' && sourceProvider !== 'icloud' && sourceProvider !== 'other') {
    items.push({provider: sourceProvider, name: sourceName, display: sourceDisplayName, role: 'source'});
  }
  if (destProvider && destProvider !== 'local' && destProvider !== 'icloud' && destProvider !== 'other') {
    items.push({provider: destProvider, name: destName, display: destDisplayName, role: 'dest'});
  }
  // Include additional multi-dest providers
  const seenProviders = new Set(items.map(i => i.name));
  multiDestinations.forEach(d => {
    if (d.provider && d.provider !== 'local' && d.provider !== 'icloud' && d.provider !== 'other') {
      const rName = providerKeys[d.provider] || d.provider;
      if (!seenProviders.has(rName)) {
        seenProviders.add(rName);
        items.push({provider: d.provider, name: rName, display: d.displayName, role: 'dest'});
      }
    }
  });

  if (items.length === 0) {
    list.innerHTML = '<div style="text-align:center; padding:20px; color:var(--text-dim);">No cloud accounts need to be connected. You\'re all set!</div>';
    document.getElementById('connectNext').disabled = false;
    return;
  }

  for (const item of items) {
    const connected = existingRemotes.includes(item.name);
    const div = document.createElement('div');
    div.className = 'connect-item';
    div.id = 'connect-' + item.name;
    div.innerHTML = `
      <div class="connect-info">
        <div class="connect-icon">${providerIcons[item.provider]}</div>
        <div>
          <div class="connect-name">${esc(item.display)}</div>
          <div class="connect-status ${connected ? 'ok' : 'pending'}" id="status-${item.name}">
            ${connected ? 'Connected' : 'Not connected'}
          </div>
        </div>
      </div>
      <div id="action-${item.name}">
        ${connected ? '<div class="checkmark">✓</div>' : ''}
      </div>
    `;
    if (!connected) {
      const btn = document.createElement('button');
      btn.className = 'btn btn-primary btn-connect';
      btn.textContent = 'Connect ' + item.display;
      btn.addEventListener('click', () => connectRemote(item.name, item.provider, item.display));
      div.lastElementChild.appendChild(btn);
    }
    list.appendChild(div);
  }
  checkAllConnected();
}

async function connectRemote(name, type, display, username, password, twofa) {
  const actionEl = document.getElementById('action-' + name);
  const statusEl = document.getElementById('status-' + name);
  actionEl.innerHTML = '<div class="spinner"></div>';
  statusEl.textContent = 'Connecting...';
  statusEl.className = 'connect-status pending';

  // Start polling as fallback for OAuth providers (may timeout but still succeed)
  if (['drive','onedrive','dropbox'].includes(type)) {
    startPolling(name, display, type);
  }

  try {
    const body = {name, type};
    if (username) body.username = username;
    if (password) body.password = password;
    if (twofa) body.twofa = twofa;
    const resp = await fetch('/api/wizard/configure-remote', {
      method: 'POST',
      headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
      body: JSON.stringify(body)
    });
    const data = await resp.json();
    if (data.ok) {
      clearPolling(name);
      statusEl.textContent = 'Connected';
      statusEl.className = 'connect-status ok';
      actionEl.innerHTML = '<div class="checkmark">✓</div>';
      if (!existingRemotes.includes(name)) existingRemotes.push(name);
    } else if (data.needs_credentials) {
      statusEl.textContent = data.msg || 'Credentials required';
      statusEl.className = 'connect-status pending';
      const userLabel = data.user_label || 'Username';
      const passLabel = data.pass_label || 'Password';
      const is2fa = type === 'protondrive';
      const formDiv = document.createElement('div');
      formDiv.style.cssText = 'display:flex;flex-direction:column;gap:8px;min-width:220px;';
      const userInput = document.createElement('input');
      userInput.className = 'form-input';
      userInput.id = 'cred-user-' + name;
      userInput.type = 'text';
      userInput.placeholder = userLabel;
      userInput.style.cssText = 'padding:8px 12px;font-size:0.8rem;';
      formDiv.appendChild(userInput);
      const passInput = document.createElement('input');
      passInput.className = 'form-input';
      passInput.id = 'cred-pass-' + name;
      passInput.type = 'password';
      passInput.placeholder = passLabel;
      passInput.style.cssText = 'padding:8px 12px;font-size:0.8rem;';
      formDiv.appendChild(passInput);
      if (is2fa) {
        const twofaInput = document.createElement('input');
        twofaInput.className = 'form-input';
        twofaInput.id = 'cred-2fa-' + name;
        twofaInput.type = 'text';
        twofaInput.placeholder = '2FA code (if enabled)';
        twofaInput.style.cssText = 'padding:8px 12px;font-size:0.8rem;';
        twofaInput.maxLength = 6;
        twofaInput.inputMode = 'numeric';
        formDiv.appendChild(twofaInput);
      }
      const credBtn = document.createElement('button');
      credBtn.className = 'btn btn-primary btn-connect';
      credBtn.textContent = 'Connect';
      credBtn.addEventListener('click', () => {
        const u = document.getElementById('cred-user-' + name).value;
        const p = document.getElementById('cred-pass-' + name).value;
        const t = is2fa ? (document.getElementById('cred-2fa-' + name) || {}).value || '' : undefined;
        connectRemote(name, type, display, u, p, t);
      });
      formDiv.appendChild(credBtn);
      actionEl.innerHTML = '';
      actionEl.appendChild(formDiv);
    } else {
      statusEl.textContent = data.msg || 'Failed to connect';
      statusEl.className = 'connect-status pending';
      actionEl.innerHTML = '';
      const retryBtn = document.createElement('button');
      retryBtn.className = 'btn btn-primary btn-connect';
      retryBtn.textContent = 'Retry';
      retryBtn.addEventListener('click', () => connectRemote(name, type, display));
      actionEl.appendChild(retryBtn);
    }
  } catch(e) {
    if (['drive','onedrive','dropbox'].includes(type)) {
      statusEl.textContent = 'Waiting for authorization...';
      statusEl.className = 'connect-status pending';
    } else {
      statusEl.textContent = 'Failed to connect';
      statusEl.className = 'connect-status pending';
      actionEl.innerHTML = '';
      const retryBtn = document.createElement('button');
      retryBtn.className = 'btn btn-primary btn-connect';
      retryBtn.textContent = 'Retry';
      retryBtn.addEventListener('click', () => connectRemote(name, type, display));
      actionEl.appendChild(retryBtn);
    }
  }
  checkAllConnected();
}

function checkAllConnected() {
  const items = document.querySelectorAll('.connect-item');
  let allOk = true;
  items.forEach(item => {
    const status = item.querySelector('.connect-status');
    if (!status.classList.contains('ok')) allOk = false;
  });
  document.getElementById('connectNext').disabled = !allOk;
}

// Poll for remote connection (for OAuth flow) - per-remote polling
const activePolls = {};
const activePollTimeouts = {};

function clearPolling(name) {
  if (activePolls[name]) { clearInterval(activePolls[name]); delete activePolls[name]; }
  if (activePollTimeouts[name]) { clearTimeout(activePollTimeouts[name]); delete activePollTimeouts[name]; }
}

function startPolling(name, display, type) {
  // Clear any existing poll and timeout for this remote
  clearPolling(name);
  const remoteName = name;
  const remoteType = type;
  activePolls[name] = setInterval(async () => {
    try {
      const resp = await fetch('/api/wizard/check-remote', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({name})
      });
      const data = await resp.json();
      if (data.configured) {
        clearPolling(name);
        const statusEl = document.getElementById('status-' + name);
        const actionEl = document.getElementById('action-' + name);
        if (statusEl) {
          statusEl.textContent = 'Connected';
          statusEl.className = 'connect-status ok';
        }
        if (actionEl) {
          actionEl.innerHTML = '<div class="checkmark">✓</div>';
        }
        if (!existingRemotes.includes(name)) existingRemotes.push(name);
        checkAllConnected();
      }
    } catch(e) {}
  }, 2000);
  // Timeout after 2 minutes
  activePollTimeouts[name] = setTimeout(() => {
    if (activePolls[name]) {
      clearPolling(name);
      const statusEl = document.getElementById('status-' + remoteName);
      const actionEl = document.getElementById('action-' + remoteName);
      if (statusEl && !statusEl.classList.contains('ok')) {
        statusEl.innerHTML = '<span style="color:var(--orange);">Timed out.</span>';
        if (actionEl) {
          actionEl.innerHTML = '';
          const retryBtn = document.createElement('button');
          retryBtn.className = 'btn btn-primary btn-connect';
          retryBtn.textContent = 'Try again';
          retryBtn.addEventListener('click', () => connectRemote(remoteName, remoteType, display));
          actionEl.appendChild(retryBtn);
        }
      }
    }
  }, 120000);
}

// Build summary
function buildSummary() {
  const card = document.getElementById('summaryCard');
  const srcSub = document.getElementById('sourceSubfolder').value.trim();
  const dstSub = document.getElementById('destSubfolder').value.trim();
  const excludes = document.getElementById('excludePatterns').value.trim();
  const bwLimit = document.getElementById('bwLimit').value.trim();
  selectedSpeed = (document.getElementById('parallelTransfers') || {}).value || selectedSpeed || '8';
  const modeLabels = {'copy': 'Copy', 'sync': 'Mirror', 'bisync': 'Two-Way Sync'};
  const useChecksum = document.getElementById('useChecksum').checked;
  const useFastList = document.getElementById('useFastList').checked;

  let srcPath = getSourcePath();
  let dstPath = getDestPath();

  const isMulti = multiSelectedPaths.length > 1;

  // Show full path for local/icloud sources instead of generic "Local Folder"
  let srcDisplay = sourceDisplayName;
  if (isMulti) {
    srcDisplay = multiSelectedPaths.length + ' sources selected';
  } else if ((sourceProvider === 'local' || sourceProvider === 'icloud') && srcPath) {
    srcDisplay = srcPath;
  } else if (srcSub) {
    srcDisplay = sourceDisplayName + ' / ' + srcSub;
  }

  let dstDisplay = destDisplayName;
  if ((destProvider === 'local' || destProvider === 'icloud') && dstPath) {
    dstDisplay = dstPath;
  } else if (dstSub) {
    dstDisplay = destDisplayName + ' / ' + dstSub;
  }

  let sourceRows = '';
  if (isMulti) {
    sourceRows = '<div class="summary-row"><span class="summary-label">Sources (' + multiSelectedPaths.length + ')</span><span class="summary-value" style="display:flex;flex-direction:column;gap:2px;">';
    multiSelectedPaths.forEach(s => {
      sourceRows += '<span style="font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(s.path) + '">' + esc(s.name || s.path) + '</span>';
    });
    sourceRows += '</span></div>';
  } else {
    sourceRows = `<div class="summary-row">
      <span class="summary-label">Source</span>
      <span class="summary-value" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(srcDisplay)}">${esc(srcDisplay)}</span>
    </div>`;
  }

  const modeColor = selectedMode === 'sync' ? 'var(--orange)' : selectedMode === 'bisync' ? 'var(--blue)' : 'var(--green)';

  const allDests = getMultiDestPaths();
  const isMultiDest = allDests.length > 1;
  let destRows = '';
  if (isMultiDest) {
    destRows = '<div class="summary-row"><span class="summary-label">Destinations (' + allDests.length + ')</span><span class="summary-value" style="display:flex;flex-direction:column;gap:2px;">';
    const allDestsDisplay = [...multiDestinations];
    if (destProvider) allDestsDisplay.push({provider: destProvider, displayName: destDisplayName, subfolder: (document.getElementById('destSubfolder') || {}).value || '', path: ''});
    allDestsDisplay.forEach(d => {
      const icon = providerIcons[d.provider] || providerIcons.other;
      const label = d.displayName + (d.subfolder ? ' / ' + d.subfolder : '');
      destRows += '<span style="font-size:0.8rem;display:flex;align-items:center;gap:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(label) + '">' + icon + ' ' + esc(label) + '</span>';
    });
    destRows += '</span></div>';
  } else {
    destRows = `<div class="summary-row">
      <span class="summary-label">Destination</span>
      <span class="summary-value" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(dstDisplay)}">${esc(dstDisplay)}</span>
    </div>`;
  }

  card.innerHTML = `
    ${sourceRows}
    ${destRows}
    <div class="summary-row">
      <span class="summary-label">Mode</span>
      <span class="summary-value" style="color:${modeColor};font-weight:700;">${esc(modeLabels[selectedMode] || 'Copy')}</span>
    </div>
    <div class="summary-row">
      <span class="summary-label">Parallel transfers</span>
      <span class="summary-value">${esc(selectedSpeed)} files</span>
    </div>
    ${excludes ? `<div class="summary-row">
      <span class="summary-label">Excluding</span>
      <span class="summary-value">${esc(excludes)}</span>
    </div>` : ''}
    ${bwLimit ? `<div class="summary-row">
      <span class="summary-label">Bandwidth Limit</span>
      <span class="summary-value">${esc(bwLimit)}</span>
    </div>` : ''}
    ${useChecksum ? `<div class="summary-row"><span class="summary-label">Checksum verification</span><span class="summary-value">Enabled</span></div>` : ''}
    ${useFastList ? `<div class="summary-row"><span class="summary-label">Fast listing</span><span class="summary-value">Enabled</span></div>` : ''}
  `;

  if (selectedMode === 'sync') {
    card.innerHTML += '<div style="margin-top:12px;padding:14px 18px;background:rgba(248,113,113,0.12);border:2px solid rgba(248,113,113,0.4);border-radius:10px;font-size:0.9rem;color:var(--red);line-height:1.6;font-weight:500;">&#9888; <strong>Mirror mode:</strong> Files at destination not in source <strong>WILL be deleted</strong>.</div>';
  } else if (selectedMode === 'bisync') {
    card.innerHTML += '<div style="margin-top:10px;padding:10px 14px;background:rgba(99,102,241,0.06);border:1px solid rgba(99,102,241,0.15);border-radius:8px;font-size:0.8rem;color:var(--blue);line-height:1.5;">&#x24D8; First sync may take longer as both sides are compared.</div>';
  }
  if (isMulti) {
    card.innerHTML += '<div style="margin-top:10px;padding:10px 14px;background:rgba(99,102,241,0.06);border:1px solid rgba(99,102,241,0.15);border-radius:8px;font-size:0.75rem;color:var(--text-dim);">Each source will be transferred sequentially via the queue.</div>';
  }
  if (isMultiDest) {
    card.innerHTML += '<div style="margin-top:10px;padding:10px 14px;background:rgba(99,102,241,0.06);border:1px solid rgba(99,102,241,0.15);border-radius:8px;font-size:0.75rem;color:var(--text-dim);">1 source &#8594; ' + allDests.length + ' destinations. Each destination will be transferred sequentially. Total estimated time: sum of individual transfers.</div>';
  }

  // Add schedule info if enabled
  const schedEl = document.getElementById('scheduleEnabled');
  if (schedEl && schedEl.checked) {
    const start = document.getElementById('scheduleStart').value;
    const end = document.getElementById('scheduleEnd').value;
    const days = [];
    document.querySelectorAll('#scheduleConfig [data-day]').forEach(cb => {
      if (cb.checked) {
        days.push(['Mon','Tue','Wed','Thu','Fri','Sat','Sun'][parseInt(cb.dataset.day)]);
      }
    });
    const bwInWindow = document.getElementById('bwLimitInWindow').value;
    let schedDesc = start + ' - ' + end + ' on ' + days.join(', ');
    if (bwInWindow) schedDesc += ' (limit: ' + bwInWindow.replace('M', ' MB/s') + ')';
    card.innerHTML += `
      <div class="summary-row">
        <span class="summary-label">Schedule</span>
        <span class="summary-value">${esc(schedDesc)}</span>
      </div>`;
  }

  // Update button text for multi-select
  const startBtn = document.getElementById('startBtn');
  if (startBtn) {
    startBtn.textContent = (isMulti || isMultiDest) ? 'Start All' : 'Start Transfer';
  }
}

function showWizardError(msg) {
  const el = document.getElementById('wizardError');
  if (el) { el.textContent = msg; el.style.display = 'block'; setTimeout(() => { el.style.display = 'none'; }, 8000); }
}

function showToast(msg) {
  const t = document.createElement('div');
  t.textContent = msg;
  t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);padding:12px 24px;background:var(--green,#22c55e);color:#fff;border-radius:10px;font-size:0.9rem;font-weight:600;z-index:1100;opacity:0;transition:opacity 0.3s;';
  document.body.appendChild(t);
  requestAnimationFrame(() => { t.style.opacity = '1'; });
  setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 3000);
}

function getSourcePath() {
  const srcSub = document.getElementById('sourceSubfolder').value.trim();
  if (sourceProvider === 'local' || sourceProvider === 'icloud') {
    const p = document.getElementById('sourcePathInput').value.trim();
    if (!p) { const errEl = document.getElementById('sourcePathError'); errEl.textContent = 'Please enter a folder path.'; errEl.style.display = 'block'; return null; }
    if (p.includes('..')) { const errEl = document.getElementById('sourcePathError'); errEl.textContent = 'Path cannot contain ".."'; errEl.style.display = 'block'; console.log('[F704] Path traversal rejected client-side'); return null; }
    document.getElementById('sourcePathError').style.display = 'none';
    return srcSub ? p + '/' + srcSub : p;
  }
  if (sourceProvider === 'other') {
    const n = document.getElementById('sourceOtherInput').value.trim();
    return n + ':' + (srcSub || '');
  }
  return sourceName + ':' + (srcSub || '');
}

function getDestPath() {
  const dstSub = document.getElementById('destSubfolder').value.trim();
  if (destProvider === 'local' || destProvider === 'icloud') {
    const p = document.getElementById('destPathInput').value.trim();
    if (!p) { const errEl = document.getElementById('destPathError'); errEl.textContent = 'Please enter a folder path.'; errEl.style.display = 'block'; return null; }
    if (p.includes('..')) { const errEl = document.getElementById('destPathError'); errEl.textContent = 'Path cannot contain ".."'; errEl.style.display = 'block'; console.log('[F704] Path traversal rejected client-side'); return null; }
    document.getElementById('destPathError').style.display = 'none';
    return dstSub ? p + '/' + dstSub : p;
  }
  if (destProvider === 'other') {
    const n = document.getElementById('destOtherInput').value.trim();
    return n + ':' + (dstSub || '');
  }
  return destName + ':' + (dstSub || '');
}

function truncatePath(path, maxLen) {
  if (!path || path.length <= maxLen) return path;
  const half = Math.floor(maxLen / 2) - 2;
  return path.substring(0, half) + '...' + path.substring(path.length - half);
}

async function previewTransfer() {
  const btn = document.getElementById('previewBtn');
  btn.disabled = true;
  btn.textContent = 'Scanning...';
  const result = document.getElementById('previewResult');
  result.style.display = 'block';
  result.innerHTML = '<div class="spinner" style="display:inline-block;margin-right:8px;vertical-align:middle;"></div> Calculating size...';

  const isMulti = multiSelectedPaths.length > 1;

  if (isMulti) {
    // Multi-select preview
    const paths = multiSelectedPaths.map(s => _getMultiSelectFullPath(s.path));
    try {
      const resp = await fetch('/api/wizard/preview-multi', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({paths: paths, source_type: sourceProvider, dest_type: destProvider, bw_limit: document.getElementById('bwLimit').value.trim()})
      });
      const data = await resp.json();
      if (data.ok) {
        let html = '<div style="margin-bottom:6px;"><strong>' + esc(data.num_sources + ' sources: ' + data.count.toLocaleString() + ' files') + '</strong> (' + esc(data.size) + ')</div>';
        data.sources.forEach(s => {
          const name = s.path.split('/').pop() || s.path;
          html += '<div style="font-size:0.75rem;color:var(--text-dim);padding:2px 0;">' + esc(name) + (s.error ? ' <span style="color:var(--red);">(' + esc(s.error) + ')</span>' : ' (' + s.files.toLocaleString() + ' files)') + '</div>';
        });
        if (data.estimated_duration) {
          html += '<div style="font-size:0.75rem;color:var(--text-dim);margin-top:6px;">Estimated time: ' + esc(data.estimated_duration) + '<br><span style="font-size:0.7rem;opacity:0.7;">Actual time depends on network speed</span></div>';
        }
        result.innerHTML = html;
      } else {
        result.innerHTML = 'Could not preview: ' + esc(data.msg || 'unknown error');
      }
    } catch(e) {
      result.innerHTML = 'Preview failed. You can still start the transfer.';
    }
  } else {
    // Single source preview
    const srcPath = getSourcePath();
    try {
      const resp = await fetch('/api/wizard/preview', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({source: srcPath, dest: getDestPath(), source_type: sourceProvider, dest_type: destProvider})
      });
      const data = await resp.json();
      if (data.ok) {
        const pathDisplay = srcPath ? truncatePath(srcPath, 60) : '';
        let html = '<div style="margin-bottom:6px;"><strong>Ready to transfer: '
          + esc(data.count.toLocaleString()) + ' files</strong> (' + esc(data.size) + ')</div>';
        if (pathDisplay) {
          html += '<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:4px;">Source: ' + esc(pathDisplay) + '</div>';
        }
        if (data.estimated_duration) {
          html += '<div style="font-size:0.75rem;color:var(--text-dim);">Estimated time: '
            + esc(data.estimated_duration)
            + '<br><span style="font-size:0.7rem;opacity:0.7;">Actual time depends on network speed</span></div>';
        }
        result.innerHTML = html;
      } else {
        result.innerHTML = 'Could not preview: ' + esc(data.msg || 'unknown error');
      }
    } catch(e) {
      result.innerHTML = 'Preview failed. You can still start the transfer.';
    }
  }
  btn.disabled = false;
  btn.textContent = 'Preview (see what will be copied)';
}

async function startTransfer() {
  if (startTransfer._running) return;
  startTransfer._running = true;
  const btn = document.getElementById('startBtn');
  if (btn.disabled) { startTransfer._running = false; return; }
  btn.disabled = true;
  btn.innerHTML = '<div class="spinner"></div> Starting transfer...';

  const safetyTimeout = setTimeout(() => {
    startTransfer._running = false;
    btn.disabled = false;
    btn.textContent = (multiSelectedPaths.length > 1 || isMultiDest()) ? 'Start All' : 'Start Transfer';
    showWizardError('Transfer may have started. Check the dashboard.');
  }, 30000);

  const excludes = document.getElementById('excludePatterns').value.trim();
  const excludeList = excludes ? excludes.split(',').map(e => e.trim()).filter(Boolean) : [];

  try {
    // Warn if destination subfolder is empty (files go to cloud root)
    const destSub = document.getElementById('destSubfolder').value.trim();
    if (!destSub && destProvider !== 'local' && destProvider !== 'icloud') {
      const proceed = await showConfirmModal('No subfolder specified. Files will be copied to the root of your cloud drive. Continue?');
      if (!proceed) {
        startTransfer._running = false;
        btn.disabled = false;
        btn.textContent = 'Start Transfer';
        return;
      }
    }
    // F506: Mirror mode confirmation
    if (selectedMode === 'sync') {
      const mirrorConfirmed = await showMirrorConfirm();
      if (!mirrorConfirmed) {
        startTransfer._running = false;
        btn.disabled = false;
        btn.textContent = (multiSelectedPaths.length > 1 || isMultiDest()) ? 'Start All' : 'Start Transfer';
        clearTimeout(safetyTimeout);
        return;
      }
    }
    const isMulti = multiSelectedPaths.length > 1;
    const allDests = getMultiDestPaths();
    const isMultiDestTransfer = allDests.length > 1;
    const dst = getDestPath();

    if (isMultiDestTransfer && !isMulti) {
      // Multi-destination: single source -> multiple destinations
      const src = getSourcePath();
      if (!src) {
        showWizardError('Please select a source folder.');
        clearTimeout(safetyTimeout);
        startTransfer._running = false;
        btn.disabled = false;
        btn.textContent = 'Start All';
        return;
      }
      const resp = await fetch('/api/wizard/start-multi-dest', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({
          source: src,
          destinations: allDests,
          transfers: selectedSpeed,
          excludes: excludeList,
          source_type: sourceProvider === 'icloud' ? 'local' : sourceProvider,
          bw_limit: document.getElementById('bwLimit').value.trim(),
          checksum: document.getElementById('useChecksum').checked,
          fast_list: document.getElementById('useFastList').checked,
          mode: selectedMode
        })
      });
      clearTimeout(safetyTimeout);
      if (!resp.ok) {
        const errText = await resp.text();
        throw new Error('HTTP ' + resp.status + ': ' + errText);
      }
      var data = await resp.json();
    } else if (isMulti) {
      // Multi-select: build full paths
      const paths = multiSelectedPaths.map(s => _getMultiSelectFullPath(s.path));
      if (!dst) {
        showWizardError('Please select a destination folder.');
        clearTimeout(safetyTimeout);
        startTransfer._running = false;
        btn.disabled = false;
        btn.textContent = 'Start All';
        return;
      }
      const resp = await fetch('/api/wizard/start-multi', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({
          paths: paths,
          dest: dst,
          transfers: selectedSpeed,
          excludes: excludeList,
          source_type: sourceProvider === 'icloud' ? 'local' : sourceProvider,
          dest_type: destProvider === 'icloud' ? 'local' : destProvider,
          bw_limit: document.getElementById('bwLimit').value.trim(),
          checksum: document.getElementById('useChecksum').checked,
          fast_list: document.getElementById('useFastList').checked,
          mode: selectedMode
        })
      });
      clearTimeout(safetyTimeout);
      if (!resp.ok) {
        const errText = await resp.text();
        throw new Error('HTTP ' + resp.status + ': ' + errText);
      }
      var data = await resp.json();
    } else {
      // Single source
      const src = getSourcePath();
      if (!src || !dst) {
        console.error('startTransfer: missing paths, src=' + src + ' dst=' + dst);
        showWizardError('Please select both source and destination folders.');
        clearTimeout(safetyTimeout);
        startTransfer._running = false;
        btn.disabled = false;
        btn.textContent = 'Start Transfer';
        return;
      }
      const resp = await fetch('/api/wizard/start', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({
          source: src,
          dest: dst,
          transfers: selectedSpeed,
          excludes: excludeList,
          source_type: sourceProvider === 'icloud' ? 'local' : sourceProvider,
          dest_type: destProvider === 'icloud' ? 'local' : destProvider,
          bw_limit: document.getElementById('bwLimit').value.trim(),
          checksum: document.getElementById('useChecksum').checked,
          fast_list: document.getElementById('useFastList').checked,
          mode: selectedMode
        })
      });
      clearTimeout(safetyTimeout);
      if (!resp.ok) {
        const errText = await resp.text();
        console.error('startTransfer: HTTP error', resp.status, errText);
        throw new Error('HTTP ' + resp.status + ': ' + errText);
      }
      var data = await resp.json();
    }
    if (data.ok) {
      // Save schedule if enabled
      const schedEnabled = document.getElementById('scheduleEnabled');
      if (schedEnabled && schedEnabled.checked) {
        const days = [];
        document.querySelectorAll('#scheduleConfig [data-day]').forEach(cb => {
          if (cb.checked) days.push(parseInt(cb.dataset.day));
        });
        await fetch('/api/schedule', {
          method: 'POST',
          headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
          body: JSON.stringify({
            enabled: true,
            start_time: document.getElementById('scheduleStart').value,
            end_time: document.getElementById('scheduleEnd').value,
            days: days,
            bw_limit_in_window: document.getElementById('bwLimitInWindow').value,
            bw_limit_out_window: '0',
          })
        });
      }
      // Redirect to dashboard
      window.location.href = '/dashboard';
    } else {
      console.error('startTransfer: server returned error:', data.msg);
      startTransfer._running = false;
      btn.disabled = false;
      btn.textContent = (multiSelectedPaths.length > 1 || isMultiDest()) ? 'Start All' : 'Start Transfer';
      showWizardError('Error: ' + (data.msg || 'Failed to start transfer'));
    }
  } catch(e) {
    console.error('startTransfer: exception:', e);
    clearTimeout(safetyTimeout);
    startTransfer._running = false;
    btn.disabled = false;
    btn.textContent = (multiSelectedPaths.length > 1 || isMultiDest()) ? 'Start All' : 'Start Transfer';
    showWizardError('Error starting transfer: ' + (e.message || 'Unknown error'));
  }
}

// Backup event listener for Start Transfer button (in case onclick is overwritten)
document.addEventListener('DOMContentLoaded', function() {
  var btn = document.getElementById('startBtn');
  if (btn && !btn._listenerAdded) {
    btn.addEventListener('click', startTransfer);
    btn._listenerAdded = true;
  }
});

// ── Preset save ─────────────────────────────────────────────────────
function _buildPresetConfig() {
  const src = getSourcePath();
  const dst = getDestPath();
  const excludes = document.getElementById('excludePatterns').value.trim();
  const excludeList = excludes ? excludes.split(',').map(e => e.trim()).filter(Boolean) : [];
  return {
    source: src || '',
    dest: dst || '',
    transfers: selectedSpeed,
    excludes: excludeList,
    source_type: sourceProvider === 'icloud' ? 'local' : (sourceProvider || ''),
    dest_type: destProvider === 'icloud' ? 'local' : (destProvider || ''),
    bw_limit: document.getElementById('bwLimit').value.trim(),
    checksum: document.getElementById('useChecksum').checked,
    fast_list: document.getElementById('useFastList').checked,
    mode: selectedMode
  };
}

function saveAsPreset() {
  const cfg = _buildPresetConfig();
  const srcLabel = (cfg.source || '').split(':')[0] || cfg.source || 'Source';
  const dstLabel = (cfg.dest || '').split(':')[0] || cfg.dest || 'Dest';
  const defaultName = srcLabel.split('/').pop() + ' \u2192 ' + dstLabel.split('/').pop();
  document.getElementById('presetNameInput').value = defaultName;
  document.getElementById('presetModal').style.display = 'flex';
  document.getElementById('presetNameInput').focus();
}

function closePresetModal() {
  document.getElementById('presetModal').style.display = 'none';
}

async function confirmSavePreset() {
  const name = document.getElementById('presetNameInput').value.trim();
  if (!name) return;
  const btn = document.getElementById('presetSaveConfirmBtn');
  btn.disabled = true;
  btn.textContent = 'Saving...';
  try {
    const resp = await fetch('/api/presets', {
      method: 'POST',
      headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
      body: JSON.stringify({name: name, config: _buildPresetConfig()})
    });
    const data = await resp.json();
    if (data.ok) {
      closePresetModal();
      showToast('Preset saved!');
    } else {
      alert(data.msg || 'Failed to save preset');
    }
  } catch(e) {
    alert('Error saving preset: ' + e.message);
  } finally {
    btn.disabled = false;
    btn.textContent = 'Save';
  }
}

// Exclude folder picker
async function browseExcludes() {
  const container = document.getElementById('excludeBrowser');
  if (!container) return;

  let basePath;
  if (sourceProvider === 'local' || sourceProvider === 'icloud') {
    basePath = document.getElementById('sourcePathInput').value.trim();
  } else {
    basePath = sourceName + ':';
  }
  if (!basePath) {
    container.style.display = 'block';
    container.innerHTML = '<div style="padding:8px;color:var(--text-muted);font-size:0.8rem;">Select a source first (go back to step 2).</div>';
    return;
  }

  container.style.display = 'block';
  container.innerHTML = '<div style="padding:8px;color:var(--text-dim);font-size:0.8rem;display:flex;align-items:center;gap:6px;"><span class="spinner"></span> Loading folders...</div>';

  try {
    const resp = await fetch('/api/wizard/browse', {
      method: 'POST',
      headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
      body: JSON.stringify({path: basePath})
    });
    const data = await resp.json();
    if (data.ok && data.folders && data.folders.length > 0) {
      const current = document.getElementById('excludePatterns').value.split(',').map(s => s.trim()).filter(Boolean);
      let html = '<div style="font-size:0.7rem;color:var(--text-dim);margin-bottom:6px;">Check folders to exclude:</div>';
      data.folders.forEach(f => {
        const checked = current.includes(f.name) ? ' checked' : '';
        html += '<label style="display:flex;align-items:center;gap:8px;padding:4px 6px;cursor:pointer;font-size:0.8rem;color:var(--text);border-radius:4px;" onmouseover="this.style.background=\'var(--card-hover)\'" onmouseout="this.style.background=\'transparent\'">';
        html += '<input type="checkbox" data-exclude="' + esc(f.name) + '" onchange="updateExcludePatterns()" style="accent-color:var(--blue);"' + checked + '>';
        html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--text-dim)" opacity="0.5"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z"/></svg>';
        html += esc(f.name) + '</label>';
      });
      container.innerHTML = html;
    } else if (data.ok && data.folders && data.folders.length === 0) {
      container.innerHTML = '<div style="padding:8px;color:var(--text-muted);font-size:0.8rem;">No folders found at source root.</div>';
    } else {
      container.innerHTML = '<div style="padding:8px;color:var(--red);font-size:0.8rem;">' + esc(data.msg || 'Could not load folders') + '</div>';
    }
  } catch(e) {
    container.innerHTML = '<div style="padding:8px;color:var(--red);font-size:0.8rem;">Error loading folders.</div>';
  }
}

function updateExcludePatterns() {
  const checks = document.querySelectorAll('#excludeBrowser input[data-exclude]:checked');
  const names = Array.from(checks).map(cb => cb.dataset.exclude);
  document.getElementById('excludePatterns').value = names.join(', ');
}

// Multi-select helpers
function _getMultiSelectFullPath(subpath) {
  let basePath;
  if (sourceProvider === 'local' || sourceProvider === 'icloud') {
    basePath = document.getElementById('sourcePathInput').value.trim();
  } else {
    basePath = sourceName + ':';
  }
  if (!subpath) return basePath;
  return basePath + (basePath.endsWith(':') ? '' : '/') + subpath;
}

function isPathSelected(fullPath) {
  return multiSelectedPaths.some(s => s.path === fullPath);
}

function togglePathSelection(fullPath, name) {
  const idx = multiSelectedPaths.findIndex(s => s.path === fullPath);
  if (idx >= 0) {
    multiSelectedPaths.splice(idx, 1);
  } else {
    multiSelectedPaths.push({path: fullPath, name: name, isFolder: true});
  }
  updateMultiSelectCounter();
}

function updateMultiSelectCounter() {
  const counter = document.getElementById('multiSelectCounter');
  if (!counter) return;
  if (multiSelectedPaths.length === 0) {
    counter.style.display = 'none';
    return;
  }
  counter.style.display = 'flex';
  const txt = document.getElementById('multiSelectCountText');
  if (txt) txt.textContent = multiSelectedPaths.length + ' item' + (multiSelectedPaths.length !== 1 ? 's' : '') + ' selected';
  const continueBtn = document.getElementById('multiSelectContinue');
  if (continueBtn) continueBtn.disabled = false;
}

function clearMultiSelect() {
  multiSelectedPaths = [];
  updateMultiSelectCounter();
  // Uncheck all visible checkboxes
  document.querySelectorAll('#browseFolders input[data-multisel]').forEach(cb => { cb.checked = false; });
}

function selectAllVisible() {
  document.querySelectorAll('#browseFolders input[data-multisel]').forEach(cb => {
    if (!cb.checked) {
      cb.checked = true;
      const fullPath = cb.dataset.multisel;
      const name = cb.dataset.name;
      if (!isPathSelected(fullPath)) {
        multiSelectedPaths.push({path: fullPath, name: name, isFolder: true});
      }
    }
  });
  updateMultiSelectCounter();
}

function deselectAllVisible() {
  document.querySelectorAll('#browseFolders input[data-multisel]').forEach(cb => {
    if (cb.checked) {
      cb.checked = false;
      const fullPath = cb.dataset.multisel;
      const idx = multiSelectedPaths.findIndex(s => s.path === fullPath);
      if (idx >= 0) multiSelectedPaths.splice(idx, 1);
    }
  });
  updateMultiSelectCounter();
}

function applyMultiSelect() {
  if (multiSelectedPaths.length === 1) {
    // Single item: backward compatible - set subfolder
    document.getElementById('sourceSubfolder').value = multiSelectedPaths[0].path;
  }
  // Multi or single, proceed to next step
  goTo(5);
}

// Folder browser for selective copy (with multi-select checkboxes)
async function browseSource(subpath) {
  const container = document.getElementById('browseFolders');
  const btn = document.getElementById('browseSourceBtn');
  if (!container) return;

  // Build the source path to browse
  let basePath;
  if (sourceProvider === 'local' || sourceProvider === 'icloud') {
    basePath = document.getElementById('sourcePathInput').value.trim();
  } else {
    basePath = sourceName + ':';
  }
  const browsePath = subpath ? basePath + (basePath.endsWith(':') ? '' : '/') + subpath : basePath;

  if (!basePath) {
    container.style.display = 'block';
    container.innerHTML = '<div style="padding:8px;color:var(--text-muted);font-size:0.8rem;">Select a source first.</div>';
    return;
  }

  container.style.display = 'block';
  if (!subpath) container.innerHTML = '<div style="padding:8px;color:var(--text-dim);font-size:0.8rem;display:flex;align-items:center;gap:6px;"><span class="spinner"></span> Loading folders...</div>';

  try {
    const resp = await fetch('/api/wizard/browse', {
      method: 'POST',
      headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
      body: JSON.stringify({path: browsePath})
    });
    const data = await resp.json();
    if (data.ok && data.folders) {
      if (data.folders.length === 0) {
        container.innerHTML = '';
        const noFolders = document.createElement('div');
        noFolders.style.cssText = 'padding:8px;color:var(--text-muted);font-size:0.8rem;';
        noFolders.textContent = 'No subfolders found.';
        if (subpath) {
          noFolders.appendChild(document.createTextNode(' '));
          const backLink = document.createElement('span');
          backLink.style.cssText = 'color:var(--blue);cursor:pointer;';
          backLink.textContent = 'Back to root';
          backLink.addEventListener('click', () => browseSource());
          noFolders.appendChild(backLink);
        }
        container.appendChild(noFolders);
        return;
      }
      container.innerHTML = '';
      // Header with Select All / Deselect All
      const headerDiv = document.createElement('div');
      headerDiv.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:4px 8px 8px;border-bottom:1px solid var(--card-border);margin-bottom:4px;';
      const headerLeft = document.createElement('div');
      headerLeft.style.cssText = 'display:flex;gap:8px;';
      const selAllBtn = document.createElement('button');
      selAllBtn.type = 'button';
      selAllBtn.style.cssText = 'padding:3px 10px;border-radius:4px;border:1px solid var(--card-border);background:var(--card);color:var(--text-dim);cursor:pointer;font-size:0.7rem;';
      selAllBtn.textContent = 'Select All';
      selAllBtn.addEventListener('click', selectAllVisible);
      const deselAllBtn = document.createElement('button');
      deselAllBtn.type = 'button';
      deselAllBtn.style.cssText = 'padding:3px 10px;border-radius:4px;border:1px solid var(--card-border);background:var(--card);color:var(--text-dim);cursor:pointer;font-size:0.7rem;';
      deselAllBtn.textContent = 'Deselect All';
      deselAllBtn.addEventListener('click', deselectAllVisible);
      headerLeft.appendChild(selAllBtn);
      headerLeft.appendChild(deselAllBtn);
      headerDiv.appendChild(headerLeft);
      container.appendChild(headerDiv);

      if (subpath) {
        const parent = subpath.includes('/') ? subpath.substring(0, subpath.lastIndexOf('/')) : '';
        const backDiv = document.createElement('div');
        backDiv.style.cssText = 'padding:6px 8px;cursor:pointer;font-size:0.8rem;color:var(--blue);display:flex;align-items:center;gap:4px;';
        backDiv.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg> Back';
        backDiv.addEventListener('click', () => browseSource(parent || undefined));
        container.appendChild(backDiv);
      }
      data.folders.forEach(f => {
        const fullPath = subpath ? subpath + '/' + f.name : f.name;
        const row = document.createElement('div');
        const isSelected = isPathSelected(fullPath);
        row.style.cssText = 'padding:6px 8px;font-size:0.8rem;color:var(--text);border-radius:6px;display:flex;align-items:center;gap:6px;' + (isSelected ? 'background:rgba(99,102,241,0.08);' : '');
        row.addEventListener('mouseover', function() { if (!isPathSelected(fullPath)) this.style.background = 'var(--card-hover)'; });
        row.addEventListener('mouseout', function() { this.style.background = isPathSelected(fullPath) ? 'rgba(99,102,241,0.08)' : 'transparent'; });
        // Checkbox
        const cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.checked = isSelected;
        cb.dataset.multisel = fullPath;
        cb.dataset.name = f.name;
        cb.style.cssText = 'accent-color:var(--primary);width:16px;height:16px;cursor:pointer;flex-shrink:0;';
        cb.addEventListener('change', function(e) {
          e.stopPropagation();
          togglePathSelection(fullPath, f.name);
          row.style.background = this.checked ? 'rgba(99,102,241,0.08)' : 'transparent';
        });
        row.appendChild(cb);
        const nameSpan = document.createElement('span');
        nameSpan.style.cssText = 'display:flex;align-items:center;gap:6px;flex:1;min-width:0;cursor:pointer;';
        nameSpan.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--blue)" opacity="0.7"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z"/></svg>';
        const nameText = document.createElement('span');
        nameText.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
        nameText.textContent = f.name;
        nameSpan.appendChild(nameText);
        nameSpan.addEventListener('click', () => browseSource(fullPath));
        row.appendChild(nameSpan);
        container.appendChild(row);
      });
    } else {
      container.innerHTML = '<div style="padding:8px;color:var(--red);font-size:0.8rem;">' + esc(data.msg || 'Could not load folders') + '</div>';
    }
  } catch(e) {
    container.innerHTML = '<div style="padding:8px;color:var(--red);font-size:0.8rem;">Error loading folders.</div>';
  }
}

// Local folder browser for source/dest path input
async function browseLocalFolder(inputId) {
  const containerId = inputId === 'sourcePathInput' ? 'sourceBrowseLocal' : 'destBrowseLocal';
  const container = document.getElementById(containerId);
  if (!container) return;

  // Toggle off if already open
  if (container.style.display === 'block') {
    container.style.display = 'none';
    return;
  }

  const input = document.getElementById(inputId);
  const startPath = (input && input.value.trim()) || window._homeDir || '/tmp';

  async function loadFolder(path) {
    container.style.display = 'block';
    container.innerHTML = '<div style="padding:8px;color:var(--text-dim);font-size:0.8rem;display:flex;align-items:center;gap:6px;"><span class="spinner"></span> Loading...</div>';

    try {
      const resp = await fetch('/api/wizard/browse', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken()},
        body: JSON.stringify({path: path})
      });
      const data = await resp.json();
      if (!data.ok) {
        container.innerHTML = '<div style="padding:8px;color:var(--red);font-size:0.8rem;">' + esc(data.msg || 'Could not load folders') + '</div>';
        return;
      }
      container.innerHTML = '';

      // Current path display + Select button
      const headerDiv = document.createElement('div');
      headerDiv.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:4px 8px 8px;border-bottom:1px solid var(--card-border);margin-bottom:4px;gap:8px;';
      const pathLabel = document.createElement('span');
      pathLabel.style.cssText = 'font-size:0.7rem;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;';
      pathLabel.textContent = path;
      pathLabel.title = path;
      headerDiv.appendChild(pathLabel);
      const selectBtn = document.createElement('button');
      selectBtn.type = 'button';
      selectBtn.className = 'btn btn-primary';
      selectBtn.style.cssText = 'padding:5px 14px;border-radius:8px;font-size:0.75rem;white-space:nowrap;flex-shrink:0;';
      selectBtn.textContent = 'Select this folder';
      selectBtn.addEventListener('click', function() {
        input.value = path;
        input.dispatchEvent(new Event('input'));
        container.style.display = 'none';
      });
      headerDiv.appendChild(selectBtn);
      container.appendChild(headerDiv);

      // Back button (go to parent)
      const parentPath = path.substring(0, path.lastIndexOf('/')) || '/';
      if (path !== '/') {
        const backDiv = document.createElement('div');
        backDiv.style.cssText = 'padding:6px 8px;cursor:pointer;font-size:0.8rem;color:var(--blue);display:flex;align-items:center;gap:4px;';
        backDiv.innerHTML = '&#8592; Back';
        backDiv.addEventListener('click', function() { loadFolder(parentPath); });
        container.appendChild(backDiv);
      }

      // Folder list
      if (data.folders && data.folders.length > 0) {
        data.folders.forEach(function(f) {
          const fullPath = path.replace(/\/$/, '') + '/' + f.name;
          const row = document.createElement('div');
          row.style.cssText = 'padding:6px 8px;font-size:0.8rem;color:var(--text);border-radius:6px;display:flex;align-items:center;gap:6px;cursor:pointer;';
          row.addEventListener('mouseover', function() { this.style.background = 'var(--card-hover)'; });
          row.addEventListener('mouseout', function() { this.style.background = 'transparent'; });
          row.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--blue)" opacity="0.7"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z"/></svg>';
          const nameSpan = document.createElement('span');
          nameSpan.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
          nameSpan.textContent = f.name;
          row.appendChild(nameSpan);
          row.addEventListener('click', function() { loadFolder(fullPath); });
          container.appendChild(row);
        });
      } else {
        const emptyDiv = document.createElement('div');
        emptyDiv.style.cssText = 'padding:8px;color:var(--text-muted);font-size:0.8rem;';
        emptyDiv.textContent = 'No subfolders found.';
        container.appendChild(emptyDiv);
      }
    } catch(e) {
      container.innerHTML = '<div style="padding:8px;color:var(--red);font-size:0.8rem;">Error browsing folders.</div>';
    }
  }

  loadFolder(startPath);
}
