/* === useExpenses(householdId) — DRAFT, not wired into index.html ===
   First per-table hook in the JSON-blob → per-table migration. Owns
   the round-trip for one collection: initial load, realtime sync,
   optimistic CRUD with rollback. Same shape we'd repeat for tasks,
   extra, poolLedger.

   Returns:
     {
       expenses,         // array, sorted by date DESC, soft-deleted filtered out
       addExpense(e),    // takes {merchant, amount, date, tagId?, memberId?, note?}, returns the row with id
       updateExpense(id, patch),
       deleteExpense(id),
       loading,
       error,
     }

   Echo-guard model:
     Optimistic local mutation immediately, plus pending-ids Set so
     when the realtime echo of our own write arrives we skip the
     setState (we already have it). The Set is keyed by id + a
     version-bumping ref to handle rapid edits. Closes the same race
     that bit us on the blob hook.

   Naming convention:
     DB columns use snake_case (tag_id, member_id). The hook surfaces
     camelCase to match every existing call site (tagId, memberId).
     toRow / fromRow do the translation in one place.
*/

function expenseFromRow(row) {
  return {
    id: row.id,
    merchant: row.merchant,
    amount: Number(row.amount),
    date: row.date,
    tagId: row.tag_id || null,
    memberId: row.member_id || null,
    note: row.note || '',
    // jsonb column; null for every expense created before the feature and
    // for the common case of no split. Normalized to [] so call sites can
    // treat it as an array without guarding (expenseNet still guards).
    reimbursements: Array.isArray(row.reimbursements) ? row.reimbursements : [],
    _updatedAt: row.updated_at,
  };
}

function expenseToRow(e, householdId) {
  return {
    id: e.id,
    household_id: householdId,
    merchant: e.merchant,
    amount: e.amount,
    date: e.date,
    tag_id: e.tagId || null,
    member_id: e.memberId || null,
    note: e.note || null,
    // Store null rather than [] so the column stays empty for the vast
    // majority of rows that never get split.
    reimbursements: (e.reimbursements && e.reimbursements.length) ? e.reimbursements : null,
  };
}

function newExpenseId() {
  return 'e' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
}

function useExpenses(householdId) {
  const [expenses, setExpenses] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  // ids of writes we just issued — used to skip the realtime echo so
  // we don't double-apply our own change on top of the optimistic update.
  // Cleared on echo receipt.
  const pendingIds = React.useRef(new Set());

  // Initial load
  React.useEffect(() => {
    if (!householdId) return;
    let cancelled = false;
    setLoading(true);
    (async () => {
      const { data, error } = await supabaseClient
        .from('expenses')
        .select('*')
        .eq('household_id', householdId)
        .is('deleted_at', null)
        .order('date', { ascending: false });
      if (cancelled) return;
      if (error) { setError(error.message); setLoading(false); return; }
      setExpenses((data || []).map(expenseFromRow));
      setError(null);
      setLoading(false);
    })();
    return () => { cancelled = true; };
  }, [householdId]);

  // Realtime: INSERT, UPDATE (covers soft-delete via deleted_at)
  React.useEffect(() => {
    if (!householdId) return;
    const channel = supabaseClient
      .channel(`expenses:${householdId}`)
      .on('postgres_changes',
          { event: 'INSERT', schema: 'public', table: 'expenses', filter: `household_id=eq.${householdId}` },
          (payload) => {
            const row = payload.new;
            // Skip the echo of our own optimistic insert.
            if (pendingIds.current.has(row.id)) {
              pendingIds.current.delete(row.id);
              return;
            }
            setExpenses(prev => {
              if (prev.some(e => e.id === row.id)) return prev;
              return [expenseFromRow(row), ...prev]
                .sort((a, b) => (a.date < b.date ? 1 : -1));
            });
          })
      .on('postgres_changes',
          { event: 'UPDATE', schema: 'public', table: 'expenses', filter: `household_id=eq.${householdId}` },
          (payload) => {
            const row = payload.new;
            if (pendingIds.current.has(row.id)) {
              pendingIds.current.delete(row.id);
              return;
            }
            setExpenses(prev => {
              if (row.deleted_at) return prev.filter(e => e.id !== row.id);
              return prev.map(e => e.id === row.id ? expenseFromRow(row) : e);
            });
          })
      .subscribe();
    return () => { supabaseClient.removeChannel(channel); };
  }, [householdId]);

  // ----- Mutations -----
  // Optimistic-first: apply locally, fire the network write, roll back on error.
  // Network failure is rare but if it happens the user sees the row disappear
  // and gets an error banner — better than silently dropping the write.

  const addExpense = React.useCallback(async (e) => {
    if (!householdId) throw new Error('No household');
    const id = e.id || newExpenseId();
    const full = { ...e, id };
    pendingIds.current.add(id);
    setExpenses(prev => [full, ...prev].sort((a, b) => (a.date < b.date ? 1 : -1)));
    const { error } = await supabaseClient
      .from('expenses')
      .insert(expenseToRow(full, householdId));
    if (error) {
      pendingIds.current.delete(id);
      setExpenses(prev => prev.filter(x => x.id !== id));
      setError(error.message);
      throw error;
    }
    setError(null);
    return full;
  }, [householdId]);

  // updateExpense: during the dual-write phase the row may exist on the
  // server (legacy, not yet backfilled into our in-memory list) but be
  // absent from local state. We always send the patch to the server in
  // that case. UPDATE matching 0 rows is a no-op, which is fine — the
  // backfill will eventually copy the latest blob state for that row.
  //
  // Patch is partial: only the fields the caller wants to change. We
  // build the column map directly (rather than via expenseToRow) so we
  // don't accidentally null-overwrite fields we didn't intend to touch.
  const updateExpense = React.useCallback(async (id, patch) => {
    if (!householdId) throw new Error('No household');
    const prevSnapshot = expenses.find(x => x.id === id) || null;
    if (prevSnapshot) {
      const next = { ...prevSnapshot, ...patch };
      pendingIds.current.add(id);
      setExpenses(prev => prev.map(x => x.id === id ? next : x));
    }
    const columnPatch = {};
    if ('merchant' in patch) columnPatch.merchant   = patch.merchant;
    if ('amount'   in patch) columnPatch.amount     = patch.amount;
    if ('date'     in patch) columnPatch.date       = patch.date;
    if ('tagId'    in patch) columnPatch.tag_id     = patch.tagId || null;
    if ('memberId' in patch) columnPatch.member_id  = patch.memberId || null;
    if ('note'     in patch) columnPatch.note       = patch.note || null;
    // Clearing every reimbursement must write null, not be skipped — the
    // `in patch` guard is what distinguishes "not editing this" from
    // "editing it to empty".
    if ('reimbursements' in patch) {
      columnPatch.reimbursements = (patch.reimbursements && patch.reimbursements.length) ? patch.reimbursements : null;
    }
    const { error } = await supabaseClient
      .from('expenses')
      .update(columnPatch)
      .eq('id', id);
    if (error) {
      if (prevSnapshot) {
        pendingIds.current.delete(id);
        setExpenses(prev => prev.map(x => x.id === id ? prevSnapshot : x));
      }
      setError(error.message);
      throw error;
    }
    setError(null);
  }, [householdId, expenses]);

  const deleteExpense = React.useCallback(async (id) => {
    if (!householdId) throw new Error('No household');
    const prevSnapshot = expenses.find(x => x.id === id) || null;
    if (prevSnapshot) {
      pendingIds.current.add(id);
      setExpenses(prev => prev.filter(x => x.id !== id));
    }
    const { error } = await supabaseClient
      .from('expenses')
      .update({ deleted_at: new Date().toISOString() })
      .eq('id', id);
    if (error) {
      if (prevSnapshot) {
        pendingIds.current.delete(id);
        setExpenses(prev => [prevSnapshot, ...prev].sort((a, b) => (a.date < b.date ? 1 : -1)));
      }
      setError(error.message);
      throw error;
    }
    setError(null);
  }, [householdId, expenses]);

  return { expenses, addExpense, updateExpense, deleteExpense, loading, error };
}

Object.assign(window, { useExpenses });
