// Supabase-backed persistence with localStorage cache.
//
// Strategy:
//   - Cache keys mirror table names: finanzas:tx, finanzas:budgets, finanzas:accounts, finanzas:prefs
//   - Reads (loadX): synchronous, return localStorage cache
//   - Writes (saveX): write cache immediately + queue async upsert to Supabase
//   - Boot: fetch all rows from Supabase, hydrate cache, then app renders
//   - On signout: clear cache
//
// Network failures fall back to cache so app keeps working offline.
// Categories stay client-only (see FINANZAS_CATEGORIES in data.jsx).

const CACHE_KEYS = {
  tx:       "finanzas:tx",
  budgets:  "finanzas:budgets",
  accounts: "finanzas:accounts",
  prefs:    "finanzas:prefs",
};

const DEFAULT_PREFS = { hide: false, dark: false, accent: "#1a2a5e" };

let _supabase = null;
let _session = null;

// Last-synced snapshots, indexed by id, used to compute diffs.
let _syncedTx = new Map();
let _syncedAcc = new Map();
let _syncedBud = new Map();

const _setSnap = (map, arr, keyFn) => {
  map.clear();
  for (const r of arr) map.set(keyFn(r), r);
};

const _notifyErr = (msg) => {
  if (typeof window !== "undefined" && window.toast?.error) window.toast.error(msg);
};

function getSupabase() {
  if (_supabase) return _supabase;
  const cfg = window.FINANZAS_CONFIG;
  if (!window.supabase || !cfg) return null;
  _supabase = window.supabase.createClient(cfg.SUPABASE_URL, cfg.SUPABASE_PUBLISHABLE_KEY, {
    auth: { persistSession: true, autoRefreshToken: true, storageKey: "finanzas:auth" },
  });
  return _supabase;
}

function safeGet(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    if (!raw) return fallback;
    return JSON.parse(raw);
  } catch (e) {
    console.warn("[finanzas] cache read failed for", key, e);
    return fallback;
  }
}

function safeSet(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
  } catch (e) {
    console.warn("[finanzas] cache write failed for", key, e);
  }
}

// ─────────────────────────────────────────────
// Row <-> client shape mappers
// Client uses `desc`, `date`, `acc`, `cat`; DB matches except `description` vs `desc`.
// ─────────────────────────────────────────────

const txRowToClient = (r) => ({
  id: r.id, date: r.date, amount: Number(r.amount),
  cat: r.cat, acc: r.acc, desc: r.description || "", tags: r.tags || [],
});
const txClientToRow = (t, userId) => ({
  user_id: userId, id: t.id, date: t.date, amount: t.amount,
  cat: t.cat, acc: t.acc, description: t.desc || "", tags: t.tags || [],
});

const accRowToClient = (r) => ({
  id: r.id, name: r.name, type: r.type, balance: Number(r.balance),
  color: r.color, last4: r.last4,
});
const accClientToRow = (a, userId) => ({
  user_id: userId, id: a.id, name: a.name, type: a.type,
  balance: a.balance, color: a.color, last4: a.last4,
});

const budgetRowToClient = (r) => ({ cat: r.cat, limit: Number(r.limit_amount), spent: Number(r.spent) });
const budgetClientToRow = (b, userId) => ({
  user_id: userId, cat: b.cat, limit_amount: b.limit, spent: b.spent,
});

// ─────────────────────────────────────────────
// Bootstrap — pull everything once at login
// ─────────────────────────────────────────────

async function bootstrap() {
  const sb = getSupabase();
  if (!sb) throw new Error("Supabase client not initialized");

  const { data: { session } } = await sb.auth.getSession();
  _session = session;
  if (!session) return false;

  const userId = session.user.id;

  const [txRes, accRes, budRes, prefRes] = await Promise.all([
    sb.from("transactions").select("*").eq("user_id", userId).order("date", { ascending: false }),
    sb.from("accounts").select("*").eq("user_id", userId),
    sb.from("budgets").select("*").eq("user_id", userId),
    sb.from("prefs").select("*").eq("user_id", userId).maybeSingle(),
  ]);

  if (txRes.error)   console.warn("[finanzas] tx fetch error", txRes.error);
  if (accRes.error)  console.warn("[finanzas] acc fetch error", accRes.error);
  if (budRes.error)  console.warn("[finanzas] budgets fetch error", budRes.error);
  if (prefRes.error) console.warn("[finanzas] prefs fetch error", prefRes.error);

  const txClients  = (txRes.data  || []).map(txRowToClient);
  const accClients = (accRes.data || []).map(accRowToClient);
  const budClients = (budRes.data || []).map(budgetRowToClient);

  safeSet(CACHE_KEYS.tx,       txClients);
  safeSet(CACHE_KEYS.accounts, accClients);
  safeSet(CACHE_KEYS.budgets,  budClients);
  safeSet(CACHE_KEYS.prefs,    prefRes.data
    ? { hide: prefRes.data.hide, dark: prefRes.data.dark, accent: prefRes.data.accent }
    : DEFAULT_PREFS);

  _setSnap(_syncedTx,  txClients,  r => r.id);
  _setSnap(_syncedAcc, accClients, r => r.id);
  _setSnap(_syncedBud, budClients, r => r.cat);

  return true;
}

// ─────────────────────────────────────────────
// Writes — cache-first, fire-and-forget upsert
// We treat the local array as source of truth for the current session;
// Supabase is the durable backing store.
// ─────────────────────────────────────────────

// Diff sync: upsert rows that changed/are new, delete rows that disappeared.
async function _diffSync({ arr, snap, table, keyFn, toRow, label, eqJSON }) {
  const sb = getSupabase(); if (!sb || !_session) return;
  const userId = _session.user.id;
  const currKeys = new Set(arr.map(keyFn));
  const removed = [];
  for (const k of snap.keys()) if (!currKeys.has(k)) removed.push(k);
  const upserts = arr.filter(r => {
    const prev = snap.get(keyFn(r));
    if (!prev) return true;
    return !eqJSON(prev, r);
  });

  let hadError = false;
  if (upserts.length) {
    const rows = upserts.map(r => toRow(r, userId));
    const { error } = await sb.from(table).upsert(rows);
    if (error) { hadError = true; console.warn(`[finanzas] ${label} upsert failed`, error); _notifyErr(`No se pudo guardar ${label}: ${error.message}`); }
  }
  if (removed.length && !hadError) {
    const idCol = table === "budgets" ? "cat" : "id";
    const { error } = await sb.from(table).delete().eq("user_id", userId).in(idCol, removed);
    if (error) { hadError = true; console.warn(`[finanzas] ${label} delete failed`, error); _notifyErr(`No se pudo eliminar de ${label}: ${error.message}`); }
  }

  if (!hadError) _setSnap(snap, arr, keyFn);
}

const _stable = (o) => JSON.stringify(Object.keys(o).sort().reduce((a, k) => (a[k] = o[k], a), {}));
const _eqClient = (a, b) => _stable(a) === _stable(b);

async function syncTx(arr) {
  await _diffSync({
    arr, snap: _syncedTx, table: "transactions",
    keyFn: r => r.id, toRow: txClientToRow, label: "transacciones", eqJSON: _eqClient,
  });
}

async function syncAccounts(arr) {
  await _diffSync({
    arr, snap: _syncedAcc, table: "accounts",
    keyFn: r => r.id, toRow: accClientToRow, label: "cuentas", eqJSON: _eqClient,
  });
}

async function syncBudgets(arr) {
  await _diffSync({
    arr, snap: _syncedBud, table: "budgets",
    keyFn: r => r.cat, toRow: budgetClientToRow, label: "presupuestos", eqJSON: _eqClient,
  });
}

async function syncPrefs(p) {
  const sb = getSupabase(); if (!sb || !_session) return;
  const userId = _session.user.id;
  const { error } = await sb.from("prefs").upsert({
    user_id: userId, hide: !!p.hide, dark: !!p.dark, accent: p.accent || "#1a2a5e",
  });
  if (error) { console.warn("[finanzas] prefs sync failed", error); _notifyErr(`No se pudo guardar preferencias: ${error.message}`); }
}

// Debounce per-key so rapid edits don't slam the network.
const _timers = {};
function debouncedSync(key, fn, value, ms = 400) {
  clearTimeout(_timers[key]);
  _timers[key] = setTimeout(() => { fn(value).catch(e => console.warn(e)); }, ms);
}

// ─────────────────────────────────────────────
// Public API — same shape as before so UI doesn't change
// ─────────────────────────────────────────────

const storage = {
  bootstrap,
  setSession(session) { _session = session; },
  clearCache() {
    Object.values(CACHE_KEYS).forEach(k => localStorage.removeItem(k));
  },

  loadTx:       () => safeGet(CACHE_KEYS.tx, []),
  saveTx:       (v) => { safeSet(CACHE_KEYS.tx, v); debouncedSync("tx", syncTx, v); },

  loadBudgets:  () => safeGet(CACHE_KEYS.budgets, []),
  saveBudgets:  (v) => { safeSet(CACHE_KEYS.budgets, v); debouncedSync("budgets", syncBudgets, v); },

  loadAccounts: () => safeGet(CACHE_KEYS.accounts, []),
  saveAccounts: (v) => { safeSet(CACHE_KEYS.accounts, v); debouncedSync("accounts", syncAccounts, v); },

  loadPrefs:    () => safeGet(CACHE_KEYS.prefs, DEFAULT_PREFS),
  savePrefs:    (v) => { safeSet(CACHE_KEYS.prefs, v); debouncedSync("prefs", syncPrefs, v, 200); },

  reset:        () => Object.values(CACHE_KEYS).forEach(k => localStorage.removeItem(k)),

  generateRecurringIfDue,

  // Auth helpers passed through
  signUp:  (email, password) => getSupabase().auth.signUp({ email, password }),
  signIn:  (email, password) => getSupabase().auth.signInWithPassword({ email, password }),
  signOut: async () => {
    const sb = getSupabase();
    await sb.auth.signOut();
    _session = null;
    _syncedTx.clear(); _syncedAcc.clear(); _syncedBud.clear();
    Object.values(CACHE_KEYS).forEach(k => localStorage.removeItem(k));
  },
  resetPassword: async (email, redirectTo) => {
    const sb = getSupabase();
    return sb.auth.resetPasswordForEmail(email, redirectTo ? { redirectTo } : undefined);
  },
  getSession: () => _session,
  onAuthChange: (cb) => getSupabase().auth.onAuthStateChange((e, s) => { _session = s; cb(e, s); }),
};

// ─────────────────────────────────────────────
// Recurring tx engine — monthly clone on boot
// ─────────────────────────────────────────────
function generateRecurringIfDue() {
  const RUN_KEY = "finanzas:recurring:last-run";
  const today = new Date().toISOString().slice(0, 10);
  try { if (localStorage.getItem(RUN_KEY) === today) return; } catch {}

  const txs = safeGet(CACHE_KEYS.tx, []);
  if (!txs.length) { try { localStorage.setItem(RUN_KEY, today); } catch {} ; return; }

  const recurring = txs.filter(t => (t.tags || []).includes("recurring"));
  if (!recurring.length) { try { localStorage.setItem(RUN_KEY, today); } catch {} ; return; }

  // Group by signature → keep most recent per group
  const groups = new Map();
  for (const t of recurring) {
    const sig = `${t.desc}|${t.cat}|${t.acc}|${t.amount}`;
    const cur = groups.get(sig);
    if (!cur || new Date(t.date) > new Date(cur.date)) groups.set(sig, t);
  }

  const now = new Date();
  const curYM = `${now.getFullYear()}-${now.getMonth()}`;
  const additions = [];
  for (const t of groups.values()) {
    const d = new Date(t.date);
    const tYM = `${d.getFullYear()}-${d.getMonth()}`;
    if (tYM === curYM) continue;
    // Already has tx this month for same signature?
    const has = txs.some(x => x !== t
      && x.desc === t.desc && x.cat === t.cat && x.acc === t.acc && x.amount === t.amount
      && new Date(x.date).getFullYear() === now.getFullYear()
      && new Date(x.date).getMonth() === now.getMonth());
    if (has) continue;
    // Clone to this month, preserving day-of-month (clamped)
    const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
    const day = Math.min(d.getDate(), lastDay);
    const newDate = new Date(now.getFullYear(), now.getMonth(), day, 12, 0).toISOString().slice(0, 16);
    additions.push({
      ...t,
      id: "rc" + Date.now() + Math.random().toString(36).slice(2, 6),
      date: newDate,
      tags: Array.from(new Set([...(t.tags || []), "auto"])),
    });
  }

  if (additions.length) {
    const next = [...additions, ...txs];
    safeSet(CACHE_KEYS.tx, next);
    debouncedSync("tx", syncTx, next);
    if (typeof window !== "undefined" && window.toast?.show) {
      window.toast.show(`${additions.length} movimiento${additions.length === 1 ? "" : "s"} recurrente${additions.length === 1 ? "" : "s"} generado${additions.length === 1 ? "" : "s"}`);
    }
  }
  try { localStorage.setItem(RUN_KEY, today); } catch {}
}

// React hook: persisted state. Reads on mount, writes on change.
function usePersistedState(loader, saver) {
  const [state, setState] = React.useState(loader);
  React.useEffect(() => { saver(state); }, [state]);
  return [state, setState];
}

Object.assign(window, { finanzasStorage: storage, usePersistedState });
