// Cloud sync layer for invoices.
//
// Behaviour summary:
//   - Source of truth on the wire: Supabase `invoices` table (one row per
//     invoice, owner-scoped via RLS).
//   - localStorage stays the snappy in-page cache. Every write to the index
//     or a per-invoice state is mirrored to Supabase asynchronously when
//     signed in, and silently no-ops when signed out.
//   - On sign-in we reconcile: pull all server rows, compare updated_at
//     against the local cache, and last-write-wins per invoice id. New
//     server rows land in the cache; new local rows get pushed.
//
// All helpers are exposed on `window` so invoice-storage.jsx and root.jsx can
// call them without an import dance. Wrapped in an IIFE so its locals don't
// collide with the shot-list cloud-sync (which uses the same helper names).

(function () {

// v2: bumped when per-account namespacing shipped, so every existing user gets
// one fresh full reconcile (pull + adopt) to resync buckets that split during
// the migration. Do NOT reuse the old key — its presence would skip the heal.
const INVOICE_SYNC_FLAG_KEY = 'pp-invoices-cloud-synced-for-v2';
const INVOICE_SYNC_TABLE    = 'invoices';

// "Am I logged in?" — derives the active user id from the Supabase client,
// falling back to null when the client isn't loaded or the session is missing.
function ppGetInvoiceUserId() {
  try {
    const c = window.supabaseClient;
    if (!c) return null;
    const u = c.auth && c.auth._lastSession?.user || null;
    if (u && u.id) return u.id;
  } catch (e) {}
  return null;
}

// Async variant — uses getSession so we always get a current answer.
async function ppGetInvoiceUserIdAsync() {
  try {
    const c = window.supabaseClient;
    if (!c) return null;
    const { data } = await c.auth.getSession();
    return data?.session?.user?.id || null;
  } catch (e) { return null; }
}

// Turn a metadata-index row + per-invoice state into a flat row for the
// Supabase `invoices` table. The state column carries the full editor JSON
// so the schema can evolve without DB migrations; the meta columns mirror
// the index-entry shape so the manager can query/sort without parsing state.
function ppInvoiceRowFromLocal(meta, state) {
  return {
    id: meta.id,
    number: meta.number || '',
    client_name: meta.clientName || '',
    invoice_date: meta.date || null,
    total: Number.isFinite(meta.total) ? meta.total : 0,
    currency: meta.currency || null,
    mode: meta.mode || 'invoice',
    paid: !!meta.paid,
    favourite: !!meta.favourite,
    archived: !!meta.archived,
    deleted_at: meta.deletedAt ? new Date(meta.deletedAt).toISOString() : null,
    state: state || {},
    // updated_at is set by the DB trigger; we send a hint anyway so the
    // server can compare against our local updatedAt during merge.
    updated_at: meta.updatedAt ? new Date(meta.updatedAt).toISOString() : new Date().toISOString(),
  };
}

// Reverse: turn a Supabase row into a (meta, state) pair we can drop into
// localStorage. The DB columns map directly back to the index-entry shape.
function ppInvoiceLocalFromRow(row) {
  const meta = {
    id: row.id,
    number: row.number || '',
    clientName: row.client_name || '',
    date: row.invoice_date || '',
    total: typeof row.total === 'number' ? row.total : Number(row.total) || 0,
    currency: row.currency || undefined,
    mode: row.mode || 'invoice',
    paid: !!row.paid,
    favourite: !!row.favourite,
    archived: !!row.archived,
    deletedAt: row.deleted_at ? new Date(row.deleted_at).getTime() : undefined,
    createdAt: row.created_at ? new Date(row.created_at).getTime() : Date.now(),
    updatedAt: row.updated_at ? new Date(row.updated_at).getTime() : Date.now(),
  };
  if (meta.currency === undefined) delete meta.currency;
  if (meta.deletedAt === undefined) delete meta.deletedAt;
  return { meta, state: row.state || {} };
}

// Push a single invoice (meta + state) to Supabase. Fire-and-forget — we
// don't block the editor on network. Errors are logged so the user can see
// them in the console without a popup interrupting their editing.
async function ppPushInvoice(id) {
  const uid = await ppGetInvoiceUserIdAsync();
  if (!uid) return;
  const c = window.supabaseClient;
  if (!c) return;
  const meta = (window.loadInvoiceIndex() || []).find(m => m.id === id);
  if (!meta) return;
  let state = {};
  try {
    const raw = localStorage.getItem(window.invoiceKey(id));
    if (raw) state = JSON.parse(raw);
  } catch (e) {}
  const row = ppInvoiceRowFromLocal(meta, state);
  row.user_id = uid;
  // Capture the server-assigned updated_at so concurrent-edit detection can
  // tell our own writes apart from another device's.
  const { data, error } = await c.from(INVOICE_SYNC_TABLE).upsert(row, { onConflict: 'id' }).select('updated_at').single();
  if (error) { console.warn('invoice cloud push failed', id, error.message); return; }
  if (data && data.updated_at) {
    window.ppInvoiceLastPushedAt = window.ppInvoiceLastPushedAt || {};
    window.ppInvoiceLastPushedAt[id] = new Date(data.updated_at).getTime();
  }
}

// Permanently remove an invoice from the cloud. Called when the local
// deleteInvoiceStorage is invoked OR when the 30-day trash purge runs.
async function ppDeleteInvoiceCloud(id) {
  const uid = await ppGetInvoiceUserIdAsync();
  if (!uid) return;
  const c = window.supabaseClient;
  if (!c) return;
  const { error } = await c.from(INVOICE_SYNC_TABLE).delete().eq('id', id);
  if (error) console.warn('invoice cloud delete failed', id, error.message);
}

// Pull every row the signed-in user owns and merge into localStorage.
//   - row present remotely but not locally → write the row locally
//   - row present in both → keep whichever has the newer updated_at
//   - row present locally but not remotely → push the local one up
//
// Returns a summary so callers can show a toast or log it.
// Move the signed-out 'anon' invoice bucket into the account's bucket on sign-in
// (the "adopt on sign-in" model). CLOUD-VERIFIED: each anon invoice is only
// adopted if we can actually own it in the cloud — an id already owned by
// ANOTHER account fails the RLS check on upsert, so we skip it rather than
// leaving a ghost that shows locally but never syncs. Runs on every sign-in so
// anon work created between sessions is always adopted. The anon bucket is
// always cleared afterwards so nothing can be re-adopted into a second account.
async function ppAdoptAnonInvoices(uid) {
  try {
    const c = window.supabaseClient;
    const anonIdxKey = 'pp-invoice-index-v1::anon';
    const raw = localStorage.getItem(anonIdxKey);
    if (!raw) return 0;
    let anonIdx = [];
    try { anonIdx = JSON.parse(raw) || []; } catch (e) {}
    const uidIdxKey = `pp-invoice-index-v1::${uid}`;
    let uidIdx = [];
    try { uidIdx = JSON.parse(localStorage.getItem(uidIdxKey) || '[]') || []; } catch (e) {}
    const have = new Set(uidIdx.map(m => m.id));
    let adopted = 0;
    for (const m of anonIdx) {
      const stKey = `pp-invoice::anon.${m.id}`;
      if (!have.has(m.id)) {
        let state = {};
        try { state = JSON.parse(localStorage.getItem(stKey) || '{}') || {}; } catch (e) {}
        let ok = true;
        if (c) {
          const row = ppInvoiceRowFromLocal(m, state);
          row.user_id = uid;
          const { error } = await c.from(INVOICE_SYNC_TABLE)
            .upsert(row, { onConflict: 'id' }).select('id').single();
          if (error) { ok = false; console.warn('anon invoice not adopted (owned elsewhere?)', m.id, error.message); }
        }
        if (ok) {
          localStorage.setItem(`pp-invoice::${uid}.${m.id}`, JSON.stringify(state));
          uidIdx.push({ ...m, updatedAt: m.updatedAt || Date.now() });
          have.add(m.id);
          adopted++;
        }
      }
      localStorage.removeItem(stKey);
    }
    localStorage.setItem(uidIdxKey, JSON.stringify(uidIdx));
    localStorage.removeItem(anonIdxKey);
    return adopted;
  } catch (e) { console.warn('adopt anon invoices failed', e); return 0; }
}

async function ppReconcileInvoicesOnSignIn() {
  const uid = await ppGetInvoiceUserIdAsync();
  if (!uid) return { ok: false, reason: 'not-signed-in' };
  const c = window.supabaseClient;
  if (!c) return { ok: false, reason: 'no-client' };

  // Point storage at THIS account's bucket, then pull any signed-out work into it.
  window.__ppInvoiceIdentity = uid;
  const adopted = await ppAdoptAnonInvoices(uid);

  // 1) Pull everything for this user
  const { data: rows, error } = await c.from(INVOICE_SYNC_TABLE).select('*').eq('user_id', uid);
  if (error) {
    console.warn('invoice cloud pull failed', error.message);
    return { ok: false, reason: 'pull-error', error: error.message };
  }
  const remoteById = new Map((rows || []).map(r => [r.id, r]));

  // 2) Walk the local index and decide per id
  const localIdx = window.loadInvoiceIndex() || [];
  const localById = new Map(localIdx.map(m => [m.id, m]));
  const summary = { pulled: 0, pushed: 0, kept: 0 };

  // a) For every remote row, decide vs local
  for (const row of (rows || [])) {
    const { meta, state } = ppInvoiceLocalFromRow(row);
    const localMeta = localById.get(row.id);
    if (!localMeta) {
      // Server has it, we don't — write to local cache.
      localIdx.push(meta);
      try { localStorage.setItem(window.invoiceKey(row.id), JSON.stringify(state)); } catch (e) {}
      summary.pulled++;
      continue;
    }
    if ((meta.updatedAt || 0) > (localMeta.updatedAt || 0)) {
      // Server is newer — overwrite local.
      const idx = localIdx.findIndex(m => m.id === row.id);
      if (idx >= 0) localIdx[idx] = meta;
      try { localStorage.setItem(window.invoiceKey(row.id), JSON.stringify(state)); } catch (e) {}
      summary.pulled++;
    } else if ((localMeta.updatedAt || 0) > (meta.updatedAt || 0)) {
      // Local is newer — push later (after the loop).
      summary.pushed++;
    } else {
      summary.kept++;
    }
  }
  window.saveInvoiceIndex(localIdx);

  // b) For every local row not on the server, push it.
  const pushes = [];
  for (const m of localIdx) {
    const remote = remoteById.get(m.id);
    if (!remote) {
      pushes.push(ppPushInvoice(m.id));
      summary.pushed++;
    } else if ((m.updatedAt || 0) > new Date(remote.updated_at).getTime()) {
      pushes.push(ppPushInvoice(m.id));
    }
  }
  await Promise.allSettled(pushes);

  // Remember that we've reconciled for this user so we don't re-run on every
  // page load — pushes during normal use keep things in sync.
  try { localStorage.setItem(INVOICE_SYNC_FLAG_KEY, uid); } catch (e) {}
  return { ok: true, adopted, ...summary };
}

// Has this user already been reconciled in this browser?
function ppInvoicesAlreadyReconciled(uid) {
  try { return localStorage.getItem(INVOICE_SYNC_FLAG_KEY) === uid; }
  catch (e) { return false; }
}

// Clear the "already reconciled" flag — used when the user signs out, so the
// next sign-in triggers a fresh reconcile (in case data drifted).
function ppClearInvoiceSyncFlag() {
  try { localStorage.removeItem(INVOICE_SYNC_FLAG_KEY); } catch (e) {}
}

// Listen for sign-in / sign-out events so cloud state stays in sync mid-
// session (e.g. user signs in from the invoice page itself). Wait until the
// Supabase client is on window, then attach. On SIGNED_IN: reconcile (once
// per uid). On SIGNED_OUT: drop the synced-for flag so the next sign-in
// re-runs the reconcile (in case data drifted).
function ppInstallInvoiceAuthListener() {
  const c = window.supabaseClient;
  if (!c) return false;
  c.auth.onAuthStateChange(async (event, session) => {
    if (event === 'SIGNED_IN' && session?.user?.id) {
      // Point storage at this account's bucket immediately so any render that
      // happens before the reconcile finishes already reads the right list.
      window.__ppInvoiceIdentity = session.user.id;
      // ALWAYS adopt first (cheap; no-op if the anon bucket is empty) so a
      // split cache from a prior version self-heals even when the once-only
      // reconcile is skipped.
      try { await ppAdoptAnonInvoices(session.user.id); } catch (e) {}
      if (!ppInvoicesAlreadyReconciled(session.user.id)) {
        try {
          const result = await ppReconcileInvoicesOnSignIn();
          console.log('invoice cloud sync:', result);
        } catch (e) { console.warn('invoice reconcile after sign-in failed', e); }
      }
      // Always nudge a re-read — the bucket just changed under the UI.
      window.dispatchEvent(new CustomEvent('pp-invoices-updated'));
    } else if (event === 'SIGNED_OUT') {
      ppClearInvoiceSyncFlag();
      // Switch the view back to the signed-out bucket so an account's invoices
      // stop showing the moment you sign out.
      window.__ppInvoiceIdentity = 'anon';
      window.dispatchEvent(new CustomEvent('pp-invoices-updated'));
    }
  });
  return true;
}
// Try immediately, retry briefly if the client isn't ready yet (auth.jsx
// might still be initializing).
if (!ppInstallInvoiceAuthListener()) {
  const t = setInterval(() => { if (ppInstallInvoiceAuthListener()) clearInterval(t); }, 100);
  setTimeout(() => clearInterval(t), 5000);
}

Object.assign(window, {
  ppGetInvoiceUserId, ppGetInvoiceUserIdAsync,
  ppPushInvoice, ppDeleteInvoiceCloud,
  ppReconcileInvoicesOnSignIn, ppAdoptAnonInvoices,
  ppInvoicesAlreadyReconciled, ppClearInvoiceSyncFlag,
});

})();
