// Portable grid-column preferences (visibility / order / width / sort).
//
// Stored in the DB settings table (so they travel with the logbook and
// survive a reinstall) AND mirrored to the WebView localStorage as a fast,
// flicker-free cache. On a fresh machine localStorage is empty, so we fall
// back to the DB copy and re-seed the cache.
import { GetUIPref, SetUIPref } from '../../wailsjs/go/main/App';

// The DB copy is ALREADY per-profile (the backend prefixes every ui.* key with
// the active profile id). The localStorage cache, however, is one namespace for
// the whole WebView, so without scoping it too a profile switch would keep
// serving the previous profile's cached layout and the correct per-profile DB
// value would never win. lsScope makes the cache per-profile as well; it's set
// once the active profile is known and updated on every profile switch.
let lsScope = '';

// setGridPrefsProfile scopes the localStorage cache to a profile. Call it before
// the grids read their state (at startup) and again whenever the active profile
// changes so each profile keeps its own column layout / widths.
export function setGridPrefsProfile(id: number | string | null | undefined): void {
  lsScope = id == null || id === '' ? '' : `p${id}.`;
  markReady();
}

// The active profile is only known after an async call, but the grids mount and
// read their state on the first paint. A grid that read before the scope was set
// looked up the UNSCOPED cache key (always a miss) and then saved under the
// scoped one — so its layout could never be restored, only rewritten. Grids now
// wait for this instead of racing it.
//
// The 5 s fallback matters: if the profile lookup fails outright, the grids must
// still come up with whatever the unscoped cache holds rather than hang with no
// columns restored.
let markReady: () => void = () => {};
const gridPrefsReady = new Promise<void>((resolve) => {
  markReady = resolve;
  setTimeout(resolve, 5000);
});

export function whenGridPrefsReady(): Promise<void> {
  return gridPrefsReady;
}

// lsKey scopes ONLY the localStorage cache key. The DB key passed to
// GetUIPref/SetUIPref is left untouched — the backend already scopes it.
function lsKey(key: string): string {
  return lsScope + key;
}

// loadLocal reads the cached column state synchronously (used in onGridReady
// to apply instantly, before the async DB round-trip).
export function loadLocal(key: string): any[] | null {
  try {
    const raw = localStorage.getItem(lsKey(key));
    const v = raw ? JSON.parse(raw) : null;
    return Array.isArray(v) ? v : null;
  } catch {
    return null;
  }
}

// loadRemote pulls the portable copy from the DB (null if none / unset).
//
// GetUIPref returns an ERROR — not "" — while the settings store is still
// coming up and not yet scoped to the active profile. Treating that as "no
// preference" was the silent data-loss path: the grid rendered defaults and the
// first column event then wrote those defaults over the good saved copy. So
// retry for a few seconds instead, and only give up on a real absence.
export async function loadRemote(key: string, attempts = 10): Promise<any[] | null> {
  for (let i = 0; i < attempts; i++) {
    try {
      const v = await GetUIPref(key);
      const parsed = v ? JSON.parse(v) : null;
      return Array.isArray(parsed) ? parsed : null;
    } catch {
      // Not ready yet (or a parse failure on a corrupt value — one more read
      // costs nothing). 300 ms × 10 covers a slow startup without hanging.
      await new Promise((r) => setTimeout(r, 300));
    }
  }
  return null;
}

// saveState write-throughs to both the cache and the DB (fire-and-forget). Only
// the cache key is profile-scoped; the DB key is scoped by the backend.
// The DB write is DEBOUNCED. AG-Grid emits a column-resized event per drag
// frame, so dragging one border used to fire dozens of settings writes; the
// cache write stays synchronous so nothing is lost if the window closes.
const pendingDB = new Map<string, string>();
const dbTimers = new Map<string, number>();

export function saveState(key: string, state: any[]) {
  const json = JSON.stringify(state);
  try { localStorage.setItem(lsKey(key), json); } catch { /* quota / private mode */ }
  pendingDB.set(key, json);
  const prev = dbTimers.get(key);
  if (prev) clearTimeout(prev);
  dbTimers.set(key, window.setTimeout(() => flushOne(key), 400));
}

function flushOne(key: string) {
  const json = pendingDB.get(key);
  dbTimers.delete(key);
  if (json == null) return;
  pendingDB.delete(key);
  SetUIPref(key, json).catch(() => {
    // The store may not be scoped yet at startup. Keep the value and try once
    // more shortly — dropping it here is how a fresh profile ended up with no
    // portable copy at all.
    pendingDB.set(key, json);
    if (!dbTimers.has(key)) dbTimers.set(key, window.setTimeout(() => flushOne(key), 2000));
  });
}

// flushGridPrefs writes any debounced state immediately. Call it when the app is
// about to close so a resize made in the last moments still reaches the DB.
export function flushGridPrefs() {
  for (const key of Array.from(pendingDB.keys())) {
    const t = dbTimers.get(key);
    if (t) clearTimeout(t);
    flushOne(key);
  }
}

// seedLocal writes a value into the cache without touching the DB (used after
// hydrating the cache from the DB on a fresh machine).
export function seedLocal(key: string, state: any[]) {
  try { localStorage.setItem(lsKey(key), JSON.stringify(state)); } catch { /* ignore */ }
}
