// Portable UI preferences.
//
// A handful of small UI prefs historically lived only in the WebView's
// localStorage, so they did NOT travel when the operator copied the OpsLog
// folder (data/) to another machine. These helpers mirror them into the DB
// settings table (ui.* keys, like the grid columns) so the whole setup is
// identical after a copy.
import { GetUIPref, SetUIPref, LogUIError } from '../../wailsjs/go/main/App';

// Keys that must travel with data/ (DB is the portable source of truth; the
// localStorage copy is just a fast, synchronous cache).
const PORTABLE_KEYS = [
  'opslog.theme',         // UI colour theme (light-warm / dark-warm / graphite / high-contrast / auto)
  'hamlog.qsoLimit',      // QSO list page size
  'bandmap.side',         // band map docked left / right
  'bandmap.show',         // band map open / closed
  'opslog.autofocusWB',   // auto-focus Worked-before
  'hamlog.filterPresets', // Filter Builder saved presets
  'opslog.showRotor',     // rotor compass shown next to the keyers
  'opslog.rotorCompact',  // rotor widget: dial + SP/LP only, no quick-turn buttons or Stop
  'opslog.decodesGridMap',// FT decodes tab: grid-square map shown beside the table
  'opslog.showChaseNew',  // Chase New panel shown next to the keyers
  'opslog.showAmpWidget', // amplifier widget shown next to the keyers
  'opslog.ampSel.widget', // which amplifier that widget shows ("all" or an amp id)
  'opslog.showBeamOnMap', // antenna beam lobe drawn on the Main map
  'opslog.startEqualsEnd',// log TIME_ON = TIME_OFF (QSO time = completion time)
  'opslog.showQsoRate',   // QSO-rate meter (10/60 min) shown in the header
  'opslog.catModeBeforeFreq', // send CAT mode before frequency (older rigs)
  'opslog.bandMapBands',  // bands shown side-by-side in the Band Map tab
  'opslog.mapAutoZoomDX', // Main map: auto-zoom to the DX (vs free pan/zoom)
  'opslog.mapView',       // Main map: remembered free-pan view (lat/lon/zoom)
  // The same, for the FT decodes map and the grid-square map: a view an
  // operator set up is theirs, and it should follow the folder like the rest.
  'opslog.ftMapView', 'opslog.gridMapView', 'opslog.satMapView',
  'opslog.lookupOnBlur',  // run the callsign lookup on blur instead of while typing
  'opslog.groupDigitalSlots', // matrix + cluster: all digital modes count as ONE (DXCC-style) instead of per-mode slots
  'opslog.clusterShowFilters', // cluster filter sidebar shown (tab + Main pane)
  // One imagery choice per map — world, grid squares, FT map, satellites.
  'opslog.mapBasemap', 'opslog.gridMapBase', 'opslog.ftmapBase', 'opslog.satMapBase',
  'opslog.dateFormat',    // how dates are DISPLAYED (iso / fr / us); storage stays ISO
  'opslog.mapGreyline',   // world map: grey line (day/night terminator) shown
  'opslog.awardRefSort', 'opslog.awardRefSortDir', // award reference table: sort column and direction
  // Cluster filter selections — restored on reopen.
  'opslog.clusterFilterSource', 'opslog.clusterGroup', 'opslog.clusterBands',
  'opslog.clusterLockBand', 'opslog.clusterLockMode', 'opslog.clusterStatusFilter',
  'opslog.clusterModeFilter', 'opslog.clusterSearch', 'opslog.clusterHideWorked',
  'opslog.distanceMiles', // distances shown in statute miles rather than km
  'opslog.iaruRegion',    // IARU region (1/2/3) — band edges and segments on the band maps
  'opslog.chasePota',     // show POTA references and the NEW POTA marker on spots
  'opslog.chaseSota',     // show SOTA references on spots
  // The other chase switches. In the DB as well as locally because AUTO-CALL
  // reads them: what the operator does not hunt is not something to transmit
  // for, and the backend cannot see localStorage.
  'opslog.chasePfx', 'opslog.chaseCounty', 'opslog.chaseState', 'opslog.chaseGrids',
  'opslog.activeTab',     // last selected tab
  'opslog.mainSplit',    // Main tab: width share of the left pane (percent) — legacy, read once to seed mainShares
  'opslog.mainShares',   // Main tab: column shares per column count, as {2:[..],3:[..],4:[..]}
  'opslog.mainQuad',     // Main tab: the 2x2 cross, as {col,row} percentages
  'opslog.clusterMuteWorked',    // cluster/band map: no colour or badge on worked spots
  'opslog.clusterSlotHighlight', // cluster/band map: colour calls not worked on this band+mode
  'opslog.bandMapWidth',    // docked band map: column width (px)
  'opslog.satSideWidth', 'opslog.satSideShown', // Satellites tab: readout column width, and whether it is shown
  'opslog.satSkyShown',  // Satellites tab: the polar sky plot
  'opslog.bandMapTabWidth', // Band map tab: shared card width (px)
  'opslog.bandMapZoom',     // band map zoom (px/kHz step) remembered per band, as one {band: index} map
  'opslog.decodeColWidths', // FT decodes table: per-column widths (px), as one {col: px} map
  'opslog.clusterMacros',   // cluster console: the twelve named command buttons
  'opslog.clusterHideSpots', // cluster console: hide the DX spot flood so replies are readable
  'opslog.clusterConsoleFollow', // cluster console: keep the view pinned to the newest line
  'opslog.gridMapColorConfirmed', 'opslog.gridMapColorWorked', // grid map: chosen fills (empty = follow the theme)
  // NOTE: 'hamlog.awardColsShown' and the grid column layouts are NOT listed here.
  // They are handled by lib/gridPrefs, which scopes the localStorage cache PER
  // PROFILE and mirrors to the DB (already per-profile) itself — mirroring them
  // through this global path would fight that per-profile scoping.
];

// syncPortablePrefs reconciles the DB with the local cache at startup:
//   • DB has a value  → copy it into localStorage (a copied folder restores it);
//   • DB empty, local set → seed the DB from local (migrates the current value).
// Call it BEFORE the first render so the app's synchronous localStorage reads
// already see the portable values — no per-component hydration needed.
export async function syncPortablePrefs(): Promise<void> {
  await Promise.all(PORTABLE_KEYS.map(async (key) => {
    try {
      const db = await GetUIPref(key);
      const local = localStorage.getItem(key);
      if (db != null && db !== '') {
        if (db !== local) { try { localStorage.setItem(key, db); } catch { /* quota */ } }
      } else if (local != null && local !== '') {
        await SetUIPref(key, local);
      }
    } catch { /* backend not ready / no DB — keep whatever is in localStorage */ }
  }));
}

// writeUiPrefDebounced is writeUiPref for a value that changes AS SOMEBODY
// TYPES.
//
// The local cache is written at once, because that is what the interface reads
// back and it costs nothing. The DATABASE write is held until the typing stops:
// writeUiPref crosses into Go and writes a row, and doing that per character in
// a text box is a round trip per keystroke — twenty-four boxes of cluster
// macros was exactly that, and it showed as characters appearing after the
// finger had left the key.
//
// Pending writes are flushed when the page goes away, so a value typed and
// immediately followed by a close is not lost.
const pendingPrefs = new Map<string, { value: string; timer: number }>();

export function writeUiPrefDebounced(key: string, value: string, ms = 400): void {
  try { localStorage.setItem(key, value); } catch { /* quota / private mode */ }
  const prev = pendingPrefs.get(key);
  if (prev) window.clearTimeout(prev.timer);
  const timer = window.setTimeout(() => {
    pendingPrefs.delete(key);
    SetUIPref(key, value).catch((e: any) => {
      try { LogUIError('ui pref', 'could not store ' + key + ': ' + String(e?.message ?? e), ''); } catch { /* nothing left to try */ }
    });
  }, ms);
  pendingPrefs.set(key, { value, timer });
}

// flushUiPrefs writes every pending value immediately.
export function flushUiPrefs(): void {
  for (const [key, p] of pendingPrefs) {
    window.clearTimeout(p.timer);
    SetUIPref(key, p.value).catch(() => { /* the local cache still holds it */ });
  }
  pendingPrefs.clear();
}
if (typeof window !== 'undefined') {
  window.addEventListener('beforeunload', flushUiPrefs);
  // Closing the app does not always fire beforeunload in a WebView; a hidden
  // page is the earlier and more reliable signal.
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') flushUiPrefs();
  });
}

// writeUiPref write-throughs a value to the local cache AND the portable DB.
// Use it everywhere these keys are written instead of localStorage.setItem.
export function writeUiPref(key: string, value: string): void {
  try { localStorage.setItem(key, value); } catch { /* quota / private mode */ }
  SetUIPref(key, value).catch((e: any) => {
    // The cache still holds it, so the interface behaves — but the BACKEND
    // reads some of these keys too (digital-mode grouping decides how the
    // cluster judges a slot). A write that reaches localStorage and not the
    // database makes the two disagree, and used to do so in silence.
    try { LogUIError("ui pref", "could not store " + key + ": " + String(e?.message ?? e), ""); } catch {}
  });
}
