Source: cloud-storage.js

// =============================================================================
// CLOUD STORAGE — Dropbox, pCloud, Box OAuth + vault backup/restore
// =============================================================================

// ---------------------------------------------------------------------------
// Cloud Activity Log — records all cloud sync transactions
// ---------------------------------------------------------------------------

var CLOUD_ACTIVITY_KEY = 'cloud_activity_log';
var CLOUD_ACTIVITY_MAX = 500;
var CLOUD_ACTIVITY_MAX_AGE_DAYS = 180;

function loadCloudActivityLog() {
  try {
    return typeof loadDataSync === 'function' ? loadDataSync(CLOUD_ACTIVITY_KEY, []) : JSON.parse(localStorage.getItem(CLOUD_ACTIVITY_KEY) || '[]');
  } catch (_) { return []; }
}

function saveCloudActivityLog(log) {
  try {
    if (typeof saveDataSync === 'function') { saveDataSync(CLOUD_ACTIVITY_KEY, log); }
    else { localStorage.setItem(CLOUD_ACTIVITY_KEY, JSON.stringify(log)); }
  } catch (e) { console.warn('[CloudStorage] Failed to save activity log', e); }
}

function recordCloudActivity(entry) {
  var log = loadCloudActivityLog();

  // Purge old entries
  var cutoff = Date.now() - CLOUD_ACTIVITY_MAX_AGE_DAYS * 86400000;
  log = log.filter(function (e) { return e.timestamp >= cutoff; });

  log.unshift({
    timestamp: Date.now(),
    action: entry.action || '',
    provider: entry.provider || '',
    result: entry.result || 'success',
    detail: entry.detail || '',
    duration: entry.duration != null ? entry.duration : null,
  });

  // Cap at max entries
  if (log.length > CLOUD_ACTIVITY_MAX) log.length = CLOUD_ACTIVITY_MAX;

  saveCloudActivityLog(log);
}

/** @type {string|null} Sort column for settings cloud activity table */
var settingsCloudSortColumn = null;
/** @type {boolean} Sort ascending for settings cloud activity table */
var settingsCloudSortAsc = true;

function renderCloudActivityTable() {
  var table = document.getElementById('settingsCloudActivityTable');
  if (!table) return;

  var data = loadCloudActivityLog();

  // Sort
  if (settingsCloudSortColumn) {
    var col = settingsCloudSortColumn;
    var asc = settingsCloudSortAsc;
    data.sort(function (a, b) {
      var valA = a[col], valB = b[col];
      if (valA < valB) return asc ? -1 : 1;
      if (valA > valB) return asc ? 1 : -1;
      return 0;
    });
  }
  // Default: newest first (already stored newest-first, but sort explicitly)
  if (!settingsCloudSortColumn) {
    data.sort(function (a, b) { return b.timestamp - a.timestamp; });
  }

  var tbody = table.querySelector('tbody');
  if (!tbody) return;

  if (data.length === 0) {
    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
    tbody.innerHTML = '<tr class="settings-log-empty"><td colspan="6">No cloud activity recorded yet.</td></tr>';
    return;
  }

  var rows = data.map(function (e) {
    var d = new Date(e.timestamp);
    var pad = function (n) { return n < 10 ? '0' + n : String(n); };
    var timeStr = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
      pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
    var resultStyle = e.result === 'fail' ? ' style="color: var(--danger, #e74c3c);"' : '';
    var durationStr = e.duration != null ? e.duration + 'ms' : '—';
    var safeDetail = sanitizeHtml(e.detail);
    return '<tr><td>' + timeStr + '</td><td>' + sanitizeHtml(e.action) + '</td><td>' + sanitizeHtml(e.provider) +
      '</td><td' + resultStyle + '>' + sanitizeHtml(e.result) + '</td><td>' + safeDetail + '</td><td>' + durationStr + '</td></tr>';
  });

  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
  tbody.innerHTML = rows.join('');

  // Sortable headers
  var cols = ['timestamp', 'action', 'provider', 'result', 'detail', 'duration'];
  table.querySelectorAll('th').forEach(function (th, idx) {
    th.style.cursor = 'pointer';
    th.onclick = function () {
      var c = cols[idx];
      if (settingsCloudSortColumn === c) {
        settingsCloudSortAsc = !settingsCloudSortAsc;
      } else {
        settingsCloudSortColumn = c;
        settingsCloudSortAsc = true;
      }
      renderCloudActivityTable();
    };
  });
}

/**
 * Render the Sync History section in Settings → Cloud.
 * Shows metadata for the pre-pull local snapshot (if any) and a restore button.
 */
function renderSyncHistorySection() {
  var container = document.getElementById('cloudSyncHistorySection');
  if (!container) return;

  var backup = null;
  try { backup = JSON.parse(localStorage.getItem('cloud_sync_override_backup') || 'null'); } catch (_) {}

  if (!backup || !backup.timestamp) {
    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
    container.innerHTML = '<p class="settings-subtext" style="margin:0">No snapshot available. A local snapshot is saved automatically before any remote pull is accepted.</p>';
    return;
  }

  var d = new Date(backup.timestamp);
  var pad = function (n) { return n < 10 ? '0' + n : String(n); };
  var timeStr = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
    ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());

  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
  container.innerHTML =
    '<div class="cloud-sync-update-meta">' +
      '<div class="cloud-sync-update-row"><span>Snapshot taken</span><strong>' + timeStr + '</strong></div>' +
      '<div class="cloud-sync-update-row"><span>Items</span><strong>' + (backup.itemCount != null ? backup.itemCount : '?') + '</strong></div>' +
      (backup.appVersion ? '<div class="cloud-sync-update-row"><span>Version</span><strong>v' + sanitizeHtml(String(backup.appVersion)) + '</strong></div>' : '') +
    '</div>' +
    '<div style="margin-top:0.6rem">' +
      '<button class="btn warning" type="button" style="font-size:0.8rem;padding:0.25rem 0.6rem" ' +
        'onclick="if(typeof syncRestoreOverrideBackup===\'function\')syncRestoreOverrideBackup();">' +
        'Restore This Snapshot' +
      '</button>' +
    '</div>';
}

async function clearCloudActivityLog() {
  const confirmed = typeof showAppConfirm === 'function'
    ? await showAppConfirm('Clear all cloud activity log? This cannot be undone.', 'Cloud Sync')
    : confirm('Clear all cloud activity log? This cannot be undone.');
  if (!confirmed) return;
  saveCloudActivityLog([]);
  var panel = document.getElementById('logPanel_cloud');
  if (panel) delete panel.dataset.rendered;
  renderCloudActivityTable();
}

/**
 * Cloud provider configurations.
 * Client IDs are placeholder — replace with real registered app IDs.
 */
const CLOUD_PROVIDERS = {
  dropbox: {
    name: 'Dropbox',
    authUrl: 'https://www.dropbox.com/oauth2/authorize',
    tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
    clientId: 'gbxz5vvffweoz4f',
    scopes: '',
    folder: '/StakTrakr',
    usePKCE: true,
    refreshable: true,
  },
  pcloud: {
    name: 'pCloud',
    authUrl: 'https://my.pcloud.com/oauth2/authorize',
    tokenUrl: 'https://api.pcloud.com/oauth2_token',
    clientId: 'TODO_REPLACE_PCLOUD_CLIENT_ID',
    scopes: '',
    folder: '/StakTrakr',
    usePKCE: false,
    refreshable: false, // pCloud tokens are lifetime
  },
  box: {
    name: 'Box',
    authUrl: 'https://account.box.com/api/oauth2/authorize',
    tokenUrl: 'https://api.box.com/oauth2/token',
    clientId: 'TODO_REPLACE_BOX_CLIENT_ID',
    scopes: '',
    folder: 'StakTrakr',
    usePKCE: false,
    refreshable: true,
  },
};

const CLOUD_REDIRECT_URI = window.location.origin + '/oauth-callback.html';

// Fallback: if we landed on index.html with OAuth params (user navigated back
// from a stale oauth-callback.html, or redirect URI was changed), capture them.
(function checkUrlForOAuthParams() {
  var params = new URLSearchParams(window.location.search);
  var code = params.get('code');
  var state = params.get('state');
  if (code) {
    history.replaceState(null, '', window.location.pathname);
    try {
      localStorage.setItem('staktrakr_oauth_result', JSON.stringify({ code: code, state: state }));
    } catch (e) { /* ignore */ }
  }
})();
const CLOUD_LATEST_FILENAME = 'staktrakr-latest.json';

// ---------------------------------------------------------------------------
// PKCE helpers
// ---------------------------------------------------------------------------

function cloudGenerateVerifier() {
  var arr = new Uint8Array(32);
  crypto.getRandomValues(arr);
  return btoa(String.fromCharCode.apply(null, arr))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function cloudGenerateChallenge(verifier) {
  var encoder = new TextEncoder();
  var digest = await crypto.subtle.digest('SHA-256', encoder.encode(verifier));
  return btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// ---------------------------------------------------------------------------
// Token management
// ---------------------------------------------------------------------------

function cloudGetStorageKey(provider) {
  return 'cloud_token_' + provider;
}

function cloudGetStoredToken(provider) {
  try {
    var raw = localStorage.getItem(cloudGetStorageKey(provider));
    return raw ? JSON.parse(raw) : null;
  } catch { return null; }
}

function cloudStoreToken(provider, tokenData) {
  localStorage.setItem(cloudGetStorageKey(provider), JSON.stringify(tokenData));
}

function cloudClearToken(provider) {
  localStorage.removeItem(cloudGetStorageKey(provider));
}

function cloudIsConnected(provider) {
  return !!cloudGetStoredToken(provider);
}

async function cloudGetToken(provider) {
  var stored = cloudGetStoredToken(provider);
  if (!stored) return null;

  var config = CLOUD_PROVIDERS[provider];

  // pCloud tokens never expire
  if (!config.refreshable) return stored.access_token;

  // Check if token is expired (with 60s buffer)
  if (stored.expires_at && Date.now() < stored.expires_at - 60000) {
    return stored.access_token;
  }

  // Attempt refresh
  if (!stored.refresh_token) {
    cloudClearToken(provider);
    if (typeof syncCloudUI === 'function') syncCloudUI();
    recordCloudActivity({ action: 'auth_fail', provider: provider, result: 'fail', detail: 'No refresh token — session expired' });
    return null;
  }

  var refreshStart = Date.now();
  try {
    var body = new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: stored.refresh_token,
      client_id: config.clientId,
    });
    var resp = await fetch(config.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body,
    });
    if (!resp.ok) throw new Error('Refresh failed');
    var data = await resp.json();
    var updated = {
      access_token: data.access_token,
      refresh_token: data.refresh_token || stored.refresh_token,
      expires_at: Date.now() + (data.expires_in || 14400) * 1000,
    };
    cloudStoreToken(provider, updated);
    recordCloudActivity({ action: 'refresh', provider: provider, result: 'success', detail: 'Token refreshed', duration: Date.now() - refreshStart });
    return updated.access_token;
  } catch (e) {
    recordCloudActivity({ action: 'refresh', provider: provider, result: 'fail', detail: String(e.message || e), duration: Date.now() - refreshStart });
    debugLog('[CloudStorage] Token refresh failed for ' + provider, e);
    cloudClearToken(provider);
    if (typeof syncCloudUI === 'function') syncCloudUI();
    return null;
  }
}

// ---------------------------------------------------------------------------
// OAuth popup flow
// ---------------------------------------------------------------------------

function cloudAuthStart(provider) {
  var config = CLOUD_PROVIDERS[provider];
  if (!config) return;

  var state = provider + '_' + Date.now();
  sessionStorage.setItem('cloud_oauth_state', state);

  var params = new URLSearchParams({
    response_type: 'code',
    client_id: config.clientId,
    redirect_uri: CLOUD_REDIRECT_URI,
    state: state,
    token_access_type: 'offline',
  });

  // Open popup synchronously (in click handler context) to avoid popup blockers,
  // then navigate it after the async PKCE challenge is computed.
  var popup = window.open('about:blank', 'staktrakr_oauth', 'width=600,height=700');

  if (config.usePKCE) {
    var verifier = cloudGenerateVerifier();
    sessionStorage.setItem('cloud_pkce_verifier', verifier);
    cloudGenerateChallenge(verifier).then(function (challenge) {
      params.set('code_challenge', challenge);
      params.set('code_challenge_method', 'S256');
      var url = config.authUrl + '?' + params.toString();
      if (popup && !popup.closed) {
        popup.location.href = url;
      } else {
        // Popup was blocked — fall back to main-window redirect
        window.location.href = url;
      }
    });
  } else {
    var url = config.authUrl + '?' + params.toString();
    if (popup && !popup.closed) {
      popup.location.href = url;
    } else {
      window.location.href = url;
    }
  }
}

// Surface auth failures to the user (toast with alert fallback).
function cloudNotifyAuthFailure(provider, message, details) {
  var providerName = (CLOUD_PROVIDERS[provider] && CLOUD_PROVIDERS[provider].name) || 'Cloud provider';
  var fullMessage = providerName + ' authentication failed: ' + message;

  recordCloudActivity({ action: 'auth_fail', provider: provider, result: 'fail', detail: message });
  debugLog('[CloudStorage] ' + fullMessage, details || '');
  if (details) {
    try { console.error('[CloudStorage] OAuth error details:', details); } catch (_) { /* ignore */ }
  }

  if (typeof showCloudToast === 'function') {
    showCloudToast(fullMessage, 7000);
  } else {
    if (typeof showAppAlert === 'function') {
      showAppAlert(fullMessage, 'Cloud Sync');
    } else {
      alert(fullMessage);
    }
  }
}

// Exchange an OAuth authorization code for an access token.
async function cloudExchangeCode(code, state) {
  var savedState = sessionStorage.getItem('cloud_oauth_state');
  var provider = (state || '').split('_')[0] || 'dropbox';

  if (state !== savedState) {
    cloudNotifyAuthFailure(provider, 'OAuth state mismatch. Please try again.');
    return;
  }

  var config = CLOUD_PROVIDERS[provider];
  if (!config) return;

  var body = new URLSearchParams({
    grant_type: 'authorization_code',
    code: code,
    client_id: config.clientId,
    redirect_uri: CLOUD_REDIRECT_URI,
  });

  var verifier = sessionStorage.getItem('cloud_pkce_verifier');
  if (verifier) {
    body.set('code_verifier', verifier);
    sessionStorage.removeItem('cloud_pkce_verifier');
  }

  try {
    var resp = await fetch(config.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body,
    });

    var raw = await resp.text();
    var data = {};
    if (raw) {
      try { data = JSON.parse(raw); } catch (_) { data = { rawResponse: raw }; }
    }

    if (!resp.ok) {
      var errText = data.error_summary || data.error_description || data.error || 'Unknown error';
      cloudNotifyAuthFailure(provider, 'Token exchange failed (' + resp.status + '). ' + String(errText), data);
      return;
    }

    if (!data.access_token) {
      cloudNotifyAuthFailure(provider, 'Token exchange response did not include an access token.', data);
      return;
    }

    var tokenData = {
      access_token: data.access_token,
      refresh_token: data.refresh_token || null,
      expires_at: data.expires_in ? Date.now() + data.expires_in * 1000 : null,
    };
    cloudStoreToken(provider, tokenData);
    sessionStorage.removeItem('cloud_oauth_state');
    if (typeof syncCloudUI === 'function') syncCloudUI();
    if (typeof showCloudToast === 'function') showCloudToast('Connected to ' + config.name + '.');
    recordCloudActivity({ action: 'connect', provider: provider, result: 'success', detail: 'Connected to ' + config.name });
    debugLog('[CloudStorage] Connected to ' + config.name);
  } catch (err) {
    cloudNotifyAuthFailure(provider, 'Token exchange request failed. Check redirect URI/domain registration and try again.', err);
  }
}

// Primary: listen for OAuth callback postMessage from popup
window.addEventListener('message', function (event) {
  if (event.origin !== window.location.origin) return;
  if (!event.data || event.data.type !== 'staktrakr-oauth') return;
  var code = event.data.code;
  var state = event.data.state;
  if (!code || !state) return;
  cloudExchangeCode(code, state);
});

// Fallback: localStorage relay when popup loses window.opener (Cloudflare challenge, etc.)
function cloudCheckOAuthRelay() {
  try {
    var raw = localStorage.getItem('staktrakr_oauth_result');
    if (!raw) return;
    localStorage.removeItem('staktrakr_oauth_result');
    var data = JSON.parse(raw);
    if (data.code && data.state) {
      cloudExchangeCode(data.code, data.state);
    }
  } catch (e) { /* ignore */ }
}

// Pick up the relay key via storage event (fires when another tab/popup writes it)
window.addEventListener('storage', function (event) {
  if (event.key === 'staktrakr_oauth_result' && event.newValue) {
    cloudCheckOAuthRelay();
  }
});

// Also check on visibility change (user returns to tab after popup closed)
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) cloudCheckOAuthRelay();
});

// Check on page load (main-window redirect lands here after oauth-callback.html)
if (document.readyState === 'complete') {
  cloudCheckOAuthRelay();
} else {
  window.addEventListener('load', cloudCheckOAuthRelay);
}

function cloudDisconnect(provider) {
  cloudClearToken(provider);
  localStorage.removeItem('cloud_last_backup');
  var providerName = (CLOUD_PROVIDERS[provider] && CLOUD_PROVIDERS[provider].name) || provider;
  recordCloudActivity({ action: 'disconnect', provider: provider, result: 'success', detail: 'Disconnected from ' + providerName });
  if (typeof syncCloudUI === 'function') syncCloudUI();
}

// ---------------------------------------------------------------------------
// Folder management (provider-specific)
// ---------------------------------------------------------------------------

async function cloudEnsureFolder(provider, token) {
  if (provider === 'dropbox') {
    // Dropbox auto-creates on upload with autorename=false
    return;
  }
  if (provider === 'pcloud') {
    // Create folder if not exists (pCloud returns existing folder if already created)
    await fetch('https://api.pcloud.com/createfolderifnotexists?path=' +
      encodeURIComponent(CLOUD_PROVIDERS[provider].folder) +
      '&access_token=' + encodeURIComponent(token));
    return;
  }
  if (provider === 'box') {
    // Check if StakTrakr folder exists at root (folder_id 0)
    var resp = await fetch('https://api.box.com/2.0/search?query=StakTrakr&type=folder&ancestor_folder_ids=0&limit=5', {
      headers: { Authorization: 'Bearer ' + token },
    });
    var data = await resp.json();
    var existing = (data.entries || []).find(function (e) { return e.name === 'StakTrakr' && e.type === 'folder'; });
    if (existing) return existing.id;
    // Create folder
    var createResp = await fetch('https://api.box.com/2.0/folders', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'StakTrakr', parent: { id: '0' } }),
    });
    var created = await createResp.json();
    return created.id;
  }
}

// ---------------------------------------------------------------------------
// Versioned filename helper
// ---------------------------------------------------------------------------

function cloudBuildVersionedFilename() {
  var d = new Date();
  var pad = function (n) { return n < 10 ? '0' + n : String(n); };
  var stamp = d.getFullYear() +
    pad(d.getMonth() + 1) + pad(d.getDate()) + '-' +
    pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());
  return 'staktrakr-backup-' + stamp + '.stvault';
}

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

function cloudSafeItemCount() {
  try { return typeof inventory !== 'undefined' ? inventory.length : 0; } catch (_) { return 0; }
}

function cloudSafeAppVersion() {
  return typeof APP_VERSION !== 'undefined' ? APP_VERSION : 'unknown';
}

// ---------------------------------------------------------------------------
// Upload vault to cloud (accepts pre-built fileBytes)
// ---------------------------------------------------------------------------

async function cloudUploadVault(provider, fileBytes) {
  var uploadStart = Date.now();
  var token = await cloudGetToken(provider);
  if (!token) throw new Error('Not connected to ' + CLOUD_PROVIDERS[provider].name);

  var config = CLOUD_PROVIDERS[provider];
  var filename = cloudBuildVersionedFilename();
  var now = Date.now();

  await cloudEnsureFolder(provider, token);

  if (provider === 'dropbox') {
    // Upload versioned backup
    var apiArg = JSON.stringify({
      path: config.folder + '/' + filename,
      mode: 'add',
      autorename: true,
      mute: true,
    });
    await fetch('https://content.dropboxapi.com/2/files/upload', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + token,
        'Content-Type': 'application/octet-stream',
        'Dropbox-API-Arg': apiArg,
      },
      body: fileBytes,
    });

    // Upload latest.json pointer
    var latestData = {
      filename: filename,
      timestamp: now,
      appVersion: cloudSafeAppVersion(),
      itemCount: cloudSafeItemCount(),
    };
    var latestBytes = new TextEncoder().encode(JSON.stringify(latestData));
    var latestArg = JSON.stringify({
      path: config.folder + '/' + CLOUD_LATEST_FILENAME,
      mode: 'overwrite',
      autorename: false,
      mute: true,
    });
    await fetch('https://content.dropboxapi.com/2/files/upload', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + token,
        'Content-Type': 'application/octet-stream',
        'Dropbox-API-Arg': latestArg,
      },
      body: latestBytes,
    });
  } else if (provider === 'pcloud') {
    var formData = new FormData();
    formData.append('file', new Blob([fileBytes]), filename);
    await fetch('https://api.pcloud.com/uploadfile?path=' +
      encodeURIComponent(config.folder) +
      '&renameifexists=1&nopartial=1&access_token=' + encodeURIComponent(token), {
      method: 'POST',
      body: formData,
    });
  } else if (provider === 'box') {
    var folderId = await cloudEnsureFolder(provider, token);
    var fd = new FormData();
    fd.append('file', new Blob([fileBytes]), filename);
    fd.append('attributes', JSON.stringify({ name: filename, parent: { id: folderId } }));
    await fetch('https://upload.box.com/api/2.0/files/content', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + token },
      body: fd,
    });
  }

  var safeCount = cloudSafeItemCount();
  var backupMeta = {
    provider: provider,
    timestamp: now,
    filename: filename,
    appVersion: cloudSafeAppVersion(),
    itemCount: safeCount,
  };
  localStorage.setItem('cloud_last_backup', JSON.stringify(backupMeta));

  recordCloudActivity({ action: 'backup', provider: provider, result: 'success', detail: filename + ' (' + safeCount + ' items)', duration: Date.now() - uploadStart });

  if (typeof syncCloudUI === 'function') syncCloudUI();
  debugLog('[CloudStorage] Backup uploaded to ' + config.name + ' as ' + filename);
}

// ---------------------------------------------------------------------------
// Download latest.json metadata from cloud
// ---------------------------------------------------------------------------

async function cloudGetRemoteLatest(provider) {
  var token = await cloudGetToken(provider);
  if (!token) return null;

  var config = CLOUD_PROVIDERS[provider];

  try {
    if (provider === 'dropbox') {
      var apiArg = JSON.stringify({ path: config.folder + '/' + CLOUD_LATEST_FILENAME });
      var resp = await fetch('https://content.dropboxapi.com/2/files/download', {
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + token,
          'Dropbox-API-Arg': apiArg,
        },
      });
      if (!resp.ok) return null;
      return resp.json();
    }
  } catch (e) {
    debugLog('[CloudStorage] Failed to fetch remote latest', e);
    return null;
  }
  return null;
}

// ---------------------------------------------------------------------------
// List backups in cloud folder
// ---------------------------------------------------------------------------

async function cloudListBackups(provider) {
  var listStart = Date.now();
  var token = await cloudGetToken(provider);
  if (!token) throw new Error('Not connected to ' + CLOUD_PROVIDERS[provider].name);

  var config = CLOUD_PROVIDERS[provider];
  var backups = [];

  if (provider === 'dropbox') {
    var resp = await fetch('https://api.dropboxapi.com/2/files/list_folder', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + token,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ path: config.folder, recursive: false }),
    });
    if (!resp.ok) {
      // Folder may not exist yet
      if (resp.status === 409) return [];
      throw new Error('List failed: ' + resp.status);
    }
    var data = await resp.json();
    var entries = data.entries || [];
    for (var i = 0; i < entries.length; i++) {
      var entry = entries[i];
      if (entry['.tag'] === 'file' && entry.name.endsWith('.stvault')) {
        backups.push({
          name: entry.name,
          server_modified: entry.server_modified,
          size: entry.size,
        });
      }
    }
  }

  // Sort newest first
  backups.sort(function (a, b) {
    return new Date(b.server_modified) - new Date(a.server_modified);
  });

  recordCloudActivity({ action: 'list', provider: provider, result: 'success', detail: backups.length + ' backups found', duration: Date.now() - listStart });

  return backups;
}

// ---------------------------------------------------------------------------
// Download a specific vault file by name
// ---------------------------------------------------------------------------

async function cloudDownloadVaultByName(provider, filename) {
  var dlStart = Date.now();
  var token = await cloudGetToken(provider);
  if (!token) throw new Error('Not connected to ' + CLOUD_PROVIDERS[provider].name);

  var config = CLOUD_PROVIDERS[provider];

  if (provider === 'dropbox') {
    var apiArg = JSON.stringify({ path: config.folder + '/' + filename });
    var resp = await fetch('https://content.dropboxapi.com/2/files/download', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + token,
        'Dropbox-API-Arg': apiArg,
      },
    });
    if (!resp.ok) throw new Error('Download failed: ' + resp.status);
    var bytes = new Uint8Array(await resp.arrayBuffer());
    var sizeKB = Math.round(bytes.byteLength / 1024);
    recordCloudActivity({ action: 'restore', provider: provider, result: 'success', detail: filename + ' (' + sizeKB + ' KB)', duration: Date.now() - dlStart });
    return bytes;
  }

  throw new Error('Download by name not supported for ' + provider);
}

// ---------------------------------------------------------------------------
// Delete a backup file from cloud
// ---------------------------------------------------------------------------

async function cloudDeleteBackup(provider, filename) {
  var deleteStart = Date.now();
  var token = await cloudGetToken(provider);
  if (!token) throw new Error('Not connected to ' + CLOUD_PROVIDERS[provider].name);

  var config = CLOUD_PROVIDERS[provider];

  if (provider === 'dropbox') {
    var resp = await fetch('https://api.dropboxapi.com/2/files/delete_v2', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + token,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ path: config.folder + '/' + filename }),
    });
    if (!resp.ok) throw new Error('Delete failed: ' + resp.status);

    // If this was also the latest pointer, clear the pointer too
    var latest = null;
    try {
      latest = JSON.parse(localStorage.getItem('cloud_last_backup'));
    } catch (_) { /* ignore */ }
    if (latest && latest.filename === filename) {
      localStorage.removeItem('cloud_last_backup');
      if (typeof syncCloudUI === 'function') syncCloudUI();
    }

    recordCloudActivity({ action: 'delete', provider: provider, result: 'success', detail: filename, duration: Date.now() - deleteStart });
    return;
  }

  throw new Error('Delete not supported for ' + provider);
}

// ---------------------------------------------------------------------------
// Download vault from cloud (legacy — downloads latest)
// ---------------------------------------------------------------------------

async function cloudDownloadVault(provider) {
  // Try to get latest pointer first
  var latest = await cloudGetRemoteLatest(provider);
  if (latest && latest.filename) {
    return cloudDownloadVaultByName(provider, latest.filename);
  }

  // Fallback: list and download newest
  var backups = await cloudListBackups(provider);
  if (backups.length > 0) {
    return cloudDownloadVaultByName(provider, backups[0].name);
  }

  throw new Error('No backups found on ' + CLOUD_PROVIDERS[provider].name);
}

// ---------------------------------------------------------------------------
// Conflict check
// ---------------------------------------------------------------------------

async function cloudCheckConflict(provider) {
  var remote = await cloudGetRemoteLatest(provider);
  var localCount = cloudSafeItemCount();
  if (!remote || !remote.timestamp) {
    return { conflict: false, local: { itemCount: localCount } };
  }

  var local = null;
  try {
    local = JSON.parse(localStorage.getItem('cloud_last_backup'));
  } catch { /* ignore */ }

  if (!local || !local.timestamp) {
    // No local record — remote is newer by definition
    return {
      conflict: true,
      reason: 'no_local_backup_record',
      remote: remote,
      local: { itemCount: localCount },
    };
  }

  if (remote.timestamp > local.timestamp) {
    return {
      conflict: true,
      reason: 'remote_newer',
      remote: remote,
      local: {
        timestamp: local.timestamp,
        lastBackupItemCount: Number(local.itemCount) || 0,
        itemCount: localCount,
      },
    };
  }

  return {
    conflict: false,
    local: {
      timestamp: local.timestamp,
      lastBackupItemCount: Number(local.itemCount) || 0,
      itemCount: localCount,
    },
  };
}

// ---------------------------------------------------------------------------
// Cloud UI sync
// ---------------------------------------------------------------------------

function syncCloudUI() {
  var lastBackup = null;
  try {
    lastBackup = JSON.parse(localStorage.getItem('cloud_last_backup'));
  } catch { /* ignore */ }

  Object.keys(CLOUD_PROVIDERS).forEach(function (key) {
    var connected = cloudIsConnected(key);
    var card = document.getElementById('cloudCard_' + key);
    if (!card) return;

    // Toggle login vs disconnect buttons
    var loginArea = card.querySelector('.cloud-login-area');
    var connectedBadge = card.querySelector('.cloud-connected-badge');
    var disconnectBtn = card.querySelector('.cloud-disconnect-btn');
    var backupListEl = document.getElementById('cloudBackupList_' + key);

    if (loginArea) loginArea.style.display = connected ? 'none' : '';
    if (connectedBadge) connectedBadge.style.display = connected ? '' : 'none';
    if (disconnectBtn) disconnectBtn.style.display = connected ? '' : 'none';

    // Enable/disable backup & restore buttons based on connection
    card.querySelectorAll('.cloud-backup-btn, .cloud-restore-btn').forEach(function (btn) {
      btn.disabled = !connected;
    });

    // Update status indicator
    var indicator = card.querySelector('.cloud-status-indicator');
    if (indicator) {
      indicator.dataset.state = connected ? 'connected' : 'disconnected';
      var textEl = indicator.querySelector('.cloud-status-text');
      if (textEl) textEl.textContent = connected ? 'Connected' : 'Not connected';
    }

    // Update sync & item count rows
    var syncEl = card.querySelector('.cloud-status-sync');
    var itemsEl = card.querySelector('.cloud-status-items');
    if (connected && lastBackup && lastBackup.provider === key) {
      var d = new Date(lastBackup.timestamp);
      if (syncEl) {
        syncEl.textContent = d.toLocaleDateString() + ' ' +
          d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
      }
      if (itemsEl) {
        var meta = lastBackup.itemCount ? lastBackup.itemCount.toLocaleString() : '0';
        if (lastBackup.appVersion) meta += ' (v' + lastBackup.appVersion + ')';
        itemsEl.textContent = meta;
      }
    } else {
      if (syncEl) syncEl.textContent = connected ? 'No backups yet' : 'Never';
      if (itemsEl) itemsEl.textContent = '\u2014';
    }

    // Update legacy status detail text
    var statusEl = card.querySelector('.cloud-status-detail');
    if (statusEl) statusEl.textContent = '';

    // Hide backup list when disconnected
    if (backupListEl && !connected) {
      backupListEl.style.display = 'none';
      backupListEl.innerHTML = '';
    }
  });

  // Update password cache status (session-only)
  var pwStatusEl = typeof safeGetElement === 'function' ? safeGetElement('cloudPwCacheStatus') : document.getElementById('cloudPwCacheStatus');
  if (pwStatusEl) {
    var cached = sessionStorage.getItem('cloud_vault_pw_cache');
    if (cached) {
      pwStatusEl.textContent = 'Cached (this session)';
      pwStatusEl.style.color = 'var(--success)';
    } else {
      pwStatusEl.textContent = 'Not cached';
      pwStatusEl.style.color = 'var(--text-secondary)';
    }
  }

  // STAK-149: Refresh auto-sync UI (toggle, last-synced, status dot)
  if (typeof refreshSyncUI === 'function') refreshSyncUI();
}

// ---------------------------------------------------------------------------
// Password cache (session-only — never persisted to localStorage)
// ---------------------------------------------------------------------------

function cloudCachePassword(provider, password) {
  var len = password.length;
  var nonce = new Uint8Array(len);
  crypto.getRandomValues(nonce);
  var encoded = new TextEncoder().encode(password);
  var data = new Uint8Array(len);
  for (var i = 0; i < len; i++) data[i] = encoded[i] ^ nonce[i];
  var payload = {
    nonce: btoa(String.fromCharCode.apply(null, nonce)),
    data: btoa(String.fromCharCode.apply(null, data)),
    provider: provider,
  };
  sessionStorage.setItem('cloud_vault_pw_cache', JSON.stringify(payload));
  _startIdleLockTimer();
}

function cloudGetCachedPassword(provider) {
  try {
    var raw = sessionStorage.getItem('cloud_vault_pw_cache');
    if (!raw) return null;
    var payload = JSON.parse(raw);
    if (payload.provider !== provider) return null;
    var nonce = Uint8Array.from(atob(payload.nonce), function (c) { return c.charCodeAt(0); });
    var data = Uint8Array.from(atob(payload.data), function (c) { return c.charCodeAt(0); });
    var decoded = new Uint8Array(data.length);
    for (var i = 0; i < data.length; i++) decoded[i] = data[i] ^ nonce[i];
    return new TextDecoder().decode(decoded);
  } catch (_) {
    return null;
  }
}

function cloudClearCachedPassword() {
  sessionStorage.removeItem('cloud_vault_pw_cache');
  _stopIdleLockTimer();
}

// ---------------------------------------------------------------------------
// Idle auto-lock: clear cached vault password after inactivity
// ---------------------------------------------------------------------------

const IDLE_LOCK_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
let _idleLockTimer = null;
let _idleThrottleTimer = null;

function _resetIdleLockTimer() {
  if (!sessionStorage.getItem('cloud_vault_pw_cache')) return;
  clearTimeout(_idleLockTimer);
  _idleLockTimer = setTimeout(function () {
    if (!sessionStorage.getItem('cloud_vault_pw_cache')) return;
    cloudClearCachedPassword();
    if (typeof showCloudToast === 'function') {
      showCloudToast('Cloud vault password cleared (idle timeout)');
    }
    debugLog('[CloudStorage] Vault password cache cleared due to inactivity');
  }, IDLE_LOCK_TIMEOUT_MS);
}

function _onUserActivity() {
  if (_idleThrottleTimer) return;
  _idleThrottleTimer = setTimeout(function () { _idleThrottleTimer = null; }, 30000);
  _resetIdleLockTimer();
}

function _startIdleLockTimer() {
  _stopIdleLockTimer();
  _resetIdleLockTimer();
  document.addEventListener('mousemove', _onUserActivity);
  document.addEventListener('keydown', _onUserActivity);
  document.addEventListener('touchstart', _onUserActivity);
}

function _stopIdleLockTimer() {
  clearTimeout(_idleLockTimer);
  clearTimeout(_idleThrottleTimer);
  _idleLockTimer = null;
  _idleThrottleTimer = null;
  document.removeEventListener('mousemove', _onUserActivity);
  document.removeEventListener('keydown', _onUserActivity);
  document.removeEventListener('touchstart', _onUserActivity);
}

// ---------------------------------------------------------------------------
// Kraken toast (easter egg on first cloud backup)
// ---------------------------------------------------------------------------

function showCloudToast(message, durationMs) {
  durationMs = durationMs || 5000;
  var toast = document.createElement('div');
  toast.className = 'cloud-toast';
  toast.textContent = message;
  document.body.appendChild(toast);
  setTimeout(function () {
    toast.classList.add('fade-out');
    toast.addEventListener('animationend', function () { toast.remove(); });
  }, durationMs);
}

function showKrakenToastIfFirst() {
  if (localStorage.getItem('cloud_kraken_seen') === 'true') return;
  localStorage.setItem('cloud_kraken_seen', 'true');
  showCloudToast('Yarr! Release the Krakens! Your treasure is encrypted and ready to brave the cloud seas. Stay vigilant, captain!');
}

// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------

window.CLOUD_PROVIDERS = CLOUD_PROVIDERS;
window.cloudAuthStart = cloudAuthStart;
window.cloudDisconnect = cloudDisconnect;
window.cloudUploadVault = cloudUploadVault;
window.cloudDownloadVault = cloudDownloadVault;
window.cloudDownloadVaultByName = cloudDownloadVaultByName;
window.cloudDeleteBackup = cloudDeleteBackup;
window.cloudListBackups = cloudListBackups;
window.cloudGetRemoteLatest = cloudGetRemoteLatest;
window.cloudCheckConflict = cloudCheckConflict;
window.cloudIsConnected = cloudIsConnected;
window.syncCloudUI = syncCloudUI;
window.cloudCachePassword = cloudCachePassword;
window.cloudGetCachedPassword = cloudGetCachedPassword;
window.cloudClearCachedPassword = cloudClearCachedPassword;
window.showCloudToast = showCloudToast;
window.showKrakenToastIfFirst = showKrakenToastIfFirst;
window.recordCloudActivity = recordCloudActivity;
window.renderCloudActivityTable = renderCloudActivityTable;
window.clearCloudActivityLog = clearCloudActivityLog;
window.renderSyncHistorySection = renderSyncHistorySection;