// 月账 · Monthly Ledger — app.js
// Several independent pieces that share the helpers below:
//   1) The dashboard's add-expense form + the income page's add-income
//      form (fetch submit, inline validation, toasts, duplicate-tap guard).
//   2) Two month pickers (dashboard, income page) — same behavior, two ids.
//   3) Two edit modals (expense, income), opened by tapping any ".tx" row
//      anywhere in the app. A row's data-kind attribute (absent = expense,
//      "income" = income) tells one shared click delegation which modal to
//      open. On the page that "owns" the row's data (dashboard for expense
//      rows, income.php for income rows), save/delete refresh in place; on
//      other pages (e.g. All Expenses, which has no summary to patch) they
//      simply reload so the list reflects the change.

(function () {
  const placeholdersByCategory = {
    'Food': ['Lunch at noodle place', 'Dumplings with colleagues', 'Coffee and a bun', 'Hotpot with friends'],
    'Groceries': ['Weekly grocery run', 'Fruit and veg at the market', 'Rice and cooking oil'],
    'Transportation': ['Bike parking', 'Metro top-up', 'Taxi to the airport'],
    'Home': ['Rent', 'New light bulbs', 'Cleaning supplies'],
    'Utilities': ['Electricity bill', 'Water bill', 'Mobile data plan'],
    'Health/Medical': ['Pharmacy', 'Dentist visit', 'Vitamins'],
    'Shopping': ['New shoes', 'Birthday gift for Mom', 'Winter jacket']
  };

  function randomPlaceholder(name) {
    const list = placeholdersByCategory[name] || ['e.g. Dinner out', 'e.g. Taxi ride', 'e.g. New shoes'];
    return list[Math.floor(Math.random() * list.length)];
  }

  function escapeHtml(value) {
    const div = document.createElement('div');
    div.textContent = value == null ? '' : String(value);
    return div.innerHTML;
  }

  // Log Dashboard only: each [data-visited-at] cell carries the visit's
  // instant as an explicit-offset ISO 8601 string (e.g. "...+08:00"), set
  // server-side from the app's canonical timezone. new Date(...) parses
  // that correctly regardless of the browser's own timezone, so re-reading
  // it back with local getters (getMonth/getDate/getHours/getMinutes)
  // naturally lands in whichever timezone the viewing device is currently
  // set to — no server-side timezone detection or stored preference
  // needed. Format matches the PHP fallback ("M j, g:ia") exactly, so
  // there's no visible reflow, only a possible time shift once this runs.
  const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

  function formatLocalVisitedAt(date) {
    const month = MONTH_ABBR[date.getMonth()];
    const day = date.getDate();
    const minutes = String(date.getMinutes()).padStart(2, '0');
    let hours = date.getHours();
    const ampm = hours >= 12 ? 'pm' : 'am';
    hours = hours % 12 || 12;
    return `${month} ${day}, ${hours}:${minutes}${ampm}`;
  }

  function initVisitorTimeLocalization() {
    const cells = document.querySelectorAll('[data-visited-at]');
    cells.forEach((cell) => {
      const date = new Date(cell.getAttribute('data-visited-at'));
      if (!isNaN(date.getTime())) {
        cell.textContent = formatLocalVisitedAt(date);
      }
    });
  }

  function toast(message, isError) {
    const container = document.getElementById('toasts');
    if (!container) {
      return;
    }
    const el = document.createElement('div');
    el.className = 'toast align-items-center border-0 ' + (isError ? 'text-bg-danger' : 'text-bg-dark');
    el.setAttribute('role', 'status');
    el.innerHTML = '<div class="d-flex"><div class="toast-body"></div>'
      + '<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button></div>';
    el.querySelector('.toast-body').textContent = message;
    container.appendChild(el);
    const t = new bootstrap.Toast(el, { delay: 3200 });
    el.addEventListener('hidden.bs.toast', () => el.remove());
    t.show();
  }

  // ---- Category grid: shared behavior for every add form + edit modal ----

  function wireCategoryGrid(grid, categoryIdInputEl, descFieldEl) {
    grid.addEventListener('click', (e) => {
      const tile = e.target.closest('.cat');
      if (!tile) {
        return;
      }
      if (tile.classList.contains('cat-more')) {
        grid.querySelectorAll('.cat.d-none').forEach((c) => c.classList.remove('d-none'));
        tile.classList.add('d-none');
        return;
      }
      grid.querySelectorAll('.cat').forEach((c) => c.classList.remove('selected'));
      tile.classList.add('selected');
      categoryIdInputEl.value = tile.dataset.id;
      if (descFieldEl) {
        descFieldEl.placeholder = randomPlaceholder(tile.dataset.name);
      }
    });
  }

  /** Programmatically selects a category tile (revealing it first if it's in the "More" set). */
  function selectCategoryInGrid(grid, categoryId) {
    const target = grid.querySelector('.cat[data-id="' + categoryId + '"]');
    if (target && target.classList.contains('d-none')) {
      grid.querySelectorAll('.cat.d-none').forEach((c) => c.classList.remove('d-none'));
      const moreBtn = grid.querySelector('.cat-more');
      if (moreBtn) {
        moreBtn.classList.add('d-none');
      }
    }
    grid.querySelectorAll('.cat').forEach((c) => c.classList.remove('selected'));
    if (target) {
      target.classList.add('selected');
    }
    return target ? target.dataset.name : null;
  }

  // ---- Field errors: shared between every add form + edit modal ----

  function clearFieldErrors(scopeForm, amountWrapEl) {
    scopeForm.querySelectorAll('.field-error').forEach((el) => el.remove());
    amountWrapEl.classList.remove('has-error');
  }

  function insertError(afterEl, message) {
    const p = document.createElement('div');
    p.className = 'field-error';
    p.textContent = message;
    afterEl.insertAdjacentElement('afterend', p);
  }

  function showFieldErrors(errors, refs) {
    clearFieldErrors(refs.form, refs.amountWrap);
    if (errors.amount) {
      refs.amountWrap.classList.add('has-error');
      insertError(refs.amountWrap, errors.amount);
    }
    if (errors.category_id) {
      insertError(refs.catGrid, errors.category_id);
    }
    // Expenses key this "expense_date", income "income_date" — never both at once.
    const dateError = errors.expense_date || errors.income_date;
    if (dateError) {
      insertError(refs.dateField, dateError);
    }
    if (errors.description) {
      insertError(refs.descField, errors.description);
    }
  }

  // ---- Dashboard refresh: summary / budget / savings / chart / recent.
  // No-op (returns false) on pages that don't have these sections. ----

  const summaryTotal = document.getElementById('summaryTotal');
  const bdtEquivalent = document.getElementById('bdtEquivalent');
  const chartBody = document.getElementById('categoryChartBody');
  const chartEmpty = document.getElementById('categoryChartEmpty');
  const recentList = document.getElementById('recentList');
  const recentEmpty = document.getElementById('recentEmpty');

  function renderChart(categories) {
    if (!categories.length) {
      chartBody.innerHTML = '';
      chartEmpty.classList.remove('d-none');
      return;
    }
    chartEmpty.classList.add('d-none');
    chartBody.innerHTML = categories.map((c) => (
      '<div class="hrow" style="--c:' + escapeHtml(c.color) + '">'
      + '<span class="hrow-ic"><i class="bi ' + escapeHtml(c.icon) + '"></i></span>'
      + '<div class="hrow-body">'
      + '<div class="hrow-top">'
      + '<span class="hname">' + escapeHtml(c.name) + '</span>'
      + '<span class="hamt tnum">' + escapeHtml(c.amount_formatted) + '</span>'
      + '<span class="hpct tnum">' + escapeHtml(c.percent) + '%</span>'
      + '</div>'
      + '<div class="htrack"><span style="width:' + escapeHtml(c.width_percent) + '%"></span></div>'
      + '</div>'
      + '</div>'
    )).join('');
  }

  function renderRecent(items) {
    if (!items.length) {
      recentList.innerHTML = '';
      recentEmpty.classList.remove('d-none');
      return;
    }
    recentEmpty.classList.add('d-none');
    recentList.innerHTML = items.map((it) => (
      '<div class="tx' + (it.editable ? '' : ' tx-locked') + '" style="--c:' + escapeHtml(it.category_color) + '"'
      + (it.editable ? ' role="button" tabindex="0"' : '')
      + ' data-editable="' + (it.editable ? '1' : '0') + '"'
      + ' data-id="' + escapeHtml(it.id) + '"'
      + ' data-amount="' + escapeHtml(it.amount) + '"'
      + ' data-expense-date="' + escapeHtml(it.expense_date) + '"'
      + ' data-category-id="' + escapeHtml(it.category_id) + '"'
      + ' data-payment-method-id="' + escapeHtml(it.payment_method_id) + '"'
      + ' data-description="' + escapeHtml(it.description) + '">'
      + '<span class="tx-ic"><i class="bi ' + escapeHtml(it.category_icon) + '"></i></span>'
      + '<div><div class="tx-desc">' + escapeHtml(it.description_display) + '</div>'
      + '<div class="tx-meta">' + escapeHtml(it.category_name) + ' · ' + escapeHtml(it.date_formatted)
      + ' · ' + escapeHtml(it.user_display_name) + '</div></div>'
      + '<div class="tx-amt tnum">' + escapeHtml(it.amount_formatted) + '</div>'
      + '</div>'
    )).join('');
  }

  // Budget line (Phase 8, redesigned as a radial ring). These elements
  // only exist in the DOM if a budget was set for the viewed month at
  // page load — budgets aren't created by expense actions, so that can't
  // change mid-session, but the guards below stay defensive regardless.
  const ringWrap = document.getElementById('ringWrap');
  const budgetRing = document.getElementById('budgetRing');
  const ringLabel = document.getElementById('ringLabel');
  const ringCircumference = 226.19;
  const budgetPlanned = document.getElementById('budgetPlanned');
  const budgetRemaining = document.getElementById('budgetRemaining');
  const budgetRemainingLabel = document.getElementById('budgetRemainingLabel');

  function renderBudget(budget) {
    if (!budgetRing || !budget) {
      return;
    }
    if (ringWrap) {
      ringWrap.classList.remove('d-none');
    }
    const pctCapped = Math.min(100, budget.percent_used);
    budgetRing.classList.toggle('over', budget.over_budget);
    budgetRing.setAttribute('stroke-dashoffset', String(ringCircumference * (1 - pctCapped / 100)));
    if (ringLabel) {
      ringLabel.textContent = budget.percent_used + '%';
      ringLabel.classList.toggle('over', budget.over_budget);
    }
    if (budgetPlanned) {
      budgetPlanned.textContent = budget.planned_formatted;
    }
    if (budgetRemaining) {
      budgetRemaining.textContent = budget.remaining_formatted;
      budgetRemaining.dataset.over = budget.over_budget ? '1' : '0';
    }
    if (budgetRemainingLabel) {
      budgetRemainingLabel.textContent = budget.over_budget ? 'Over by' : 'Left';
    }
  }

  /** Returns true if this page has a dashboard to refresh; false elsewhere (caller should reload instead). */
  function applyDashboardRefresh(data) {
    if (!summaryTotal) {
      return false;
    }
    summaryTotal.textContent = data.summary.total_formatted;
    if (bdtEquivalent) {
      bdtEquivalent.classList.toggle('d-none', !data.summary.bdt_formatted);
      bdtEquivalent.textContent = data.summary.bdt_formatted || '';
    }
    renderBudget(data.summary.budget);
    renderChart(data.categories);
    renderRecent(data.recent);
    return true;
  }

  // ---- Income page refresh: month total + full income list ----

  const incomeMonthTotal = document.getElementById('incomeMonthTotal');
  const incomeListEl = document.getElementById('incomeList');
  const incomeListEmpty = document.getElementById('incomeListEmpty');

  function renderIncomeList(items) {
    if (!items.length) {
      incomeListEl.innerHTML = '';
      incomeListEmpty.classList.remove('d-none');
      return;
    }
    incomeListEmpty.classList.add('d-none');
    incomeListEl.innerHTML = items.map((it) => (
      '<div class="tx" style="--c:' + escapeHtml(it.category_color) + '" role="button" tabindex="0" data-kind="income"'
      + ' data-id="' + escapeHtml(it.id) + '"'
      + ' data-amount="' + escapeHtml(it.amount) + '"'
      + ' data-income-date="' + escapeHtml(it.income_date) + '"'
      + ' data-category-id="' + escapeHtml(it.category_id) + '"'
      + ' data-description="' + escapeHtml(it.description) + '">'
      + '<span class="tx-ic"><i class="bi ' + escapeHtml(it.category_icon) + '"></i></span>'
      + '<div><div class="tx-desc">' + escapeHtml(it.description_display) + '</div>'
      + '<div class="tx-meta">' + escapeHtml(it.category_name) + ' · ' + escapeHtml(it.date_formatted)
      + ' · ' + escapeHtml(it.user_display_name) + '</div></div>'
      + '<div class="tx-amt tnum" style="color:var(--good);">+' + escapeHtml(it.amount_formatted) + '</div>'
      + '</div>'
    )).join('');
  }

  /** Returns true if this page has the income list to refresh; false elsewhere. */
  function applyIncomeDashboardRefresh(data) {
    if (!incomeMonthTotal) {
      return false;
    }
    incomeMonthTotal.textContent = data.summary.total_formatted;
    renderIncomeList(data.incomes);
    return true;
  }

  // ==========================================================================
  // Add forms — dashboard's add-expense, income page's add-income
  // ==========================================================================

  function initAddForm() {
    const form = document.getElementById('addForm');
    if (!form) {
      return;
    }

    const amountInput = document.getElementById('amount');
    const amountWrap = document.getElementById('amountWrap');
    const catGrid = document.getElementById('catGrid');
    const categoryIdInput = document.getElementById('categoryId');
    const descField = document.getElementById('desc');
    const dateField = document.getElementById('date');
    const methodField = document.getElementById('method');
    const saveBtn = document.getElementById('saveBtn');

    wireCategoryGrid(catGrid, categoryIdInput, descField);

    // The date field's initial value was rendered server-side (today's date
    // if viewing the current month, otherwise the 1st of the viewed month —
    // Phase 5). Only current-month viewing needs a live re-sync on reset, to
    // cover a tab left open across midnight; a past/future month's default
    // never goes stale, so it's left exactly as rendered.
    const isCurrentMonth = form.dataset.isCurrentMonth === '1';

    descField.placeholder = randomPlaceholder(getSelectedCategoryName());

    function getSelectedCategoryName() {
      const selected = catGrid.querySelector('.cat.selected');
      return selected ? selected.dataset.name : 'Food';
    }

    function resetAddForm() {
      form.reset();
      catGrid.querySelectorAll('.cat').forEach((c) => c.classList.remove('selected'));
      const first = catGrid.querySelector('.cat:not(.cat-more)');
      if (first) {
        first.classList.add('selected');
        categoryIdInput.value = first.dataset.id;
        descField.placeholder = randomPlaceholder(first.dataset.name);
      }
      if (isCurrentMonth) {
        dateField.valueAsDate = new Date();
      }
      if (methodField.options.length) {
        methodField.selectedIndex = 0;
      }
      clearFieldErrors(form, amountWrap);
      amountInput.focus();
    }

    let addSubmitting = false;

    form.addEventListener('submit', (e) => {
      e.preventDefault();
      if (addSubmitting) {
        return;
      }
      addSubmitting = true;
      saveBtn.disabled = true;
      const originalLabel = saveBtn.textContent;
      saveBtn.textContent = 'Saving...';
      clearFieldErrors(form, amountWrap);

      fetch(form.action, {
        method: 'POST',
        body: new FormData(form),
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      })
        .then((res) => res.json())
        .then((data) => {
          if (data.session_expired) {
            window.location.href = 'login.php';
            return;
          }
          if (!data.success) {
            if (data.errors) {
              showFieldErrors(data.errors, { form, amountWrap, catGrid, dateField, descField });
            } else {
              toast(data.error || "Couldn't save — please try again.", true);
            }
            return;
          }
          applyDashboardRefresh(data);
          toast('Saved — ' + data.toast);
          resetAddForm();
        })
        .catch(() => {
          toast("Couldn't reach the server — check your connection and try again.", true);
        })
        .finally(() => {
          addSubmitting = false;
          saveBtn.disabled = false;
          saveBtn.textContent = originalLabel;
        });
    });
  }

  function initAddIncomeForm() {
    const form = document.getElementById('addIncomeForm');
    if (!form) {
      return;
    }

    const amountInput = document.getElementById('incomeAmount');
    const amountWrap = document.getElementById('incomeAmountWrap');
    const catGrid = document.getElementById('incomeCatGrid');
    const categoryIdInput = document.getElementById('incomeCategoryId');
    const descField = document.getElementById('incomeDesc');
    const dateField = document.getElementById('incomeDate');
    const saveBtn = document.getElementById('incomeSaveBtn');

    wireCategoryGrid(catGrid, categoryIdInput, null);

    const isCurrentMonth = form.dataset.isCurrentMonth === '1';

    function resetIncomeForm() {
      form.reset();
      catGrid.querySelectorAll('.cat').forEach((c) => c.classList.remove('selected'));
      const first = catGrid.querySelector('.cat:not(.cat-more)');
      if (first) {
        first.classList.add('selected');
        categoryIdInput.value = first.dataset.id;
      }
      if (isCurrentMonth) {
        dateField.valueAsDate = new Date();
      }
      clearFieldErrors(form, amountWrap);
      amountInput.focus();
    }

    let submitting = false;

    form.addEventListener('submit', (e) => {
      e.preventDefault();
      if (submitting) {
        return;
      }
      submitting = true;
      saveBtn.disabled = true;
      const originalLabel = saveBtn.textContent;
      saveBtn.textContent = 'Saving...';
      clearFieldErrors(form, amountWrap);

      fetch(form.action, {
        method: 'POST',
        body: new FormData(form),
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      })
        .then((res) => res.json())
        .then((data) => {
          if (data.session_expired) {
            window.location.href = 'login.php';
            return;
          }
          if (!data.success) {
            if (data.errors) {
              showFieldErrors(data.errors, { form, amountWrap, catGrid, dateField, descField });
            } else {
              toast(data.error || "Couldn't save — please try again.", true);
            }
            return;
          }
          applyIncomeDashboardRefresh(data);
          toast('Saved — ' + data.toast);
          resetIncomeForm();
        })
        .catch(() => {
          toast("Couldn't reach the server — check your connection and try again.", true);
        })
        .finally(() => {
          submitting = false;
          saveBtn.disabled = false;
          saveBtn.textContent = originalLabel;
        });
    });
  }

  function initMonthPicker(pickerId, formId) {
    // Tapping the "Spent/Income in <Month>" label opens the native month
    // picker (label-for focuses it even though it's visually hidden);
    // changing it navigates via a real GET request.
    const monthPicker = document.getElementById(pickerId);
    if (!monthPicker) {
      return;
    }
    monthPicker.addEventListener('change', () => {
      if (monthPicker.value) {
        document.getElementById(formId).submit();
      }
    });
  }

  // ==========================================================================
  // Edit modals — expense and income. A single shared click delegation
  // (initRowClickDelegation, below) routes to whichever is open based on
  // the tapped row's data-kind.
  // ==========================================================================

  let openExpenseEditModal = null;
  let openIncomeEditModalFn = null;

  function initEditModal() {
    const editModalEl = document.getElementById('editModal');
    if (!editModalEl) {
      return;
    }

    const editModal = bootstrap.Modal.getOrCreateInstance(editModalEl);
    const editForm = document.getElementById('editForm');
    const editId = document.getElementById('editId');
    const editAmount = document.getElementById('editAmount');
    const editAmountWrap = document.getElementById('editAmountWrap');
    const editCatGrid = document.getElementById('editCatGrid');
    const editCategoryId = document.getElementById('editCategoryId');
    const editDesc = document.getElementById('editDesc');
    const editDate = document.getElementById('editDate');
    const editMethod = document.getElementById('editMethod');
    const editSaveBtn = document.getElementById('editSaveBtn');
    const deleteBtn = document.getElementById('deleteBtn');

    wireCategoryGrid(editCatGrid, editCategoryId, null);

    function openEditModal(row) {
      editForm.reset();
      clearFieldErrors(editForm, editAmountWrap);
      editId.value = row.dataset.id;
      editAmount.value = row.dataset.amount;
      editDate.value = row.dataset.expenseDate;
      editDesc.value = row.dataset.description;
      editMethod.value = row.dataset.paymentMethodId || '';
      selectCategoryInGrid(editCatGrid, row.dataset.categoryId);
      editModal.show();
    }
    openExpenseEditModal = openEditModal;

    function afterChange(data) {
      if (!applyDashboardRefresh(data)) {
        window.location.reload();
      }
    }

    let editSubmitting = false;

    editForm.addEventListener('submit', (e) => {
      e.preventDefault();
      if (editSubmitting) {
        return;
      }
      editSubmitting = true;
      editSaveBtn.disabled = true;
      const originalLabel = editSaveBtn.textContent;
      editSaveBtn.textContent = 'Saving...';
      clearFieldErrors(editForm, editAmountWrap);

      fetch(editForm.action, {
        method: 'POST',
        body: new FormData(editForm),
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      })
        .then((res) => res.json())
        .then((data) => {
          if (data.session_expired) {
            window.location.href = 'login.php';
            return;
          }
          if (!data.success) {
            if (data.errors) {
              showFieldErrors(data.errors, {
                form: editForm, amountWrap: editAmountWrap, catGrid: editCatGrid, dateField: editDate, descField: editDesc
              });
            } else {
              toast(data.error || "Couldn't save — please try again.", true);
            }
            return;
          }
          toast('Updated — ' + data.toast);
          editModal.hide();
          afterChange(data);
        })
        .catch(() => {
          toast("Couldn't reach the server — check your connection and try again.", true);
        })
        .finally(() => {
          editSubmitting = false;
          editSaveBtn.disabled = false;
          editSaveBtn.textContent = originalLabel;
        });
    });

    deleteBtn.addEventListener('click', () => {
      const categoryName = editCatGrid.querySelector('.cat.selected')?.dataset.name || 'this expense';
      const amountText = editAmount.value ? editAmountWrap.querySelector('.amount-cur').textContent + editAmount.value : 'it';
      const dateText = editDate.value ? new Date(editDate.value + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
      const confirmed = window.confirm('Delete ' + amountText + ' ' + categoryName + (dateText ? ' from ' + dateText : '') + '?');
      if (!confirmed) {
        return;
      }

      deleteBtn.disabled = true;

      const formData = new FormData();
      formData.append('id', editId.value);
      formData.append('csrf_token', editForm.querySelector('[name="csrf_token"]').value);
      formData.append('viewed_month', editForm.querySelector('[name="viewed_month"]').value);

      fetch('api/expense_delete.php', {
        method: 'POST',
        body: formData,
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      })
        .then((res) => res.json())
        .then((data) => {
          if (data.session_expired) {
            window.location.href = 'login.php';
            return;
          }
          if (!data.success) {
            toast(data.error || "Couldn't delete — please try again.", true);
            deleteBtn.disabled = false;
            return;
          }
          toast('Deleted — ' + categoryName + ' ' + amountText);
          editModal.hide();
          afterChange(data);
        })
        .catch(() => {
          toast("Couldn't reach the server — check your connection and try again.", true);
          deleteBtn.disabled = false;
        });
    });
  }

  function initIncomeEditModal() {
    const editModalEl = document.getElementById('editIncomeModal');
    if (!editModalEl) {
      return;
    }

    const editModal = bootstrap.Modal.getOrCreateInstance(editModalEl);
    const editForm = document.getElementById('editIncomeForm');
    const editId = document.getElementById('editIncomeId');
    const editAmount = document.getElementById('editIncomeAmount');
    const editAmountWrap = document.getElementById('editIncomeAmountWrap');
    const editCatGrid = document.getElementById('editIncomeCatGrid');
    const editCategoryId = document.getElementById('editIncomeCategoryId');
    const editDesc = document.getElementById('editIncomeDesc');
    const editDate = document.getElementById('editIncomeDate');
    const editSaveBtn = document.getElementById('editIncomeSaveBtn');
    const deleteBtn = document.getElementById('deleteIncomeBtn');

    wireCategoryGrid(editCatGrid, editCategoryId, null);

    function openIncomeEditModal(row) {
      editForm.reset();
      clearFieldErrors(editForm, editAmountWrap);
      editId.value = row.dataset.id;
      editAmount.value = row.dataset.amount;
      editDate.value = row.dataset.incomeDate;
      editDesc.value = row.dataset.description;
      selectCategoryInGrid(editCatGrid, row.dataset.categoryId);
      editModal.show();
    }
    openIncomeEditModalFn = openIncomeEditModal;

    function afterChange(data) {
      if (!applyIncomeDashboardRefresh(data)) {
        window.location.reload();
      }
    }

    let editSubmitting = false;

    editForm.addEventListener('submit', (e) => {
      e.preventDefault();
      if (editSubmitting) {
        return;
      }
      editSubmitting = true;
      editSaveBtn.disabled = true;
      const originalLabel = editSaveBtn.textContent;
      editSaveBtn.textContent = 'Saving...';
      clearFieldErrors(editForm, editAmountWrap);

      fetch(editForm.action, {
        method: 'POST',
        body: new FormData(editForm),
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      })
        .then((res) => res.json())
        .then((data) => {
          if (data.session_expired) {
            window.location.href = 'login.php';
            return;
          }
          if (!data.success) {
            if (data.errors) {
              showFieldErrors(data.errors, {
                form: editForm, amountWrap: editAmountWrap, catGrid: editCatGrid, dateField: editDate, descField: editDesc
              });
            } else {
              toast(data.error || "Couldn't save — please try again.", true);
            }
            return;
          }
          toast('Updated — ' + data.toast);
          editModal.hide();
          afterChange(data);
        })
        .catch(() => {
          toast("Couldn't reach the server — check your connection and try again.", true);
        })
        .finally(() => {
          editSubmitting = false;
          editSaveBtn.disabled = false;
          editSaveBtn.textContent = originalLabel;
        });
    });

    deleteBtn.addEventListener('click', () => {
      const categoryName = editCatGrid.querySelector('.cat.selected')?.dataset.name || 'this income';
      const amountText = editAmount.value ? editAmountWrap.querySelector('.amount-cur').textContent + editAmount.value : 'it';
      const dateText = editDate.value ? new Date(editDate.value + 'T00:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
      const confirmed = window.confirm('Delete ' + amountText + ' ' + categoryName + (dateText ? ' from ' + dateText : '') + '?');
      if (!confirmed) {
        return;
      }

      deleteBtn.disabled = true;

      const formData = new FormData();
      formData.append('id', editId.value);
      formData.append('csrf_token', editForm.querySelector('[name="csrf_token"]').value);
      formData.append('viewed_month', editForm.querySelector('[name="viewed_month"]').value);

      fetch('api/income_delete.php', {
        method: 'POST',
        body: formData,
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      })
        .then((res) => res.json())
        .then((data) => {
          if (data.session_expired) {
            window.location.href = 'login.php';
            return;
          }
          if (!data.success) {
            toast(data.error || "Couldn't delete — please try again.", true);
            deleteBtn.disabled = false;
            return;
          }
          toast('Deleted — ' + categoryName + ' ' + amountText);
          editModal.hide();
          afterChange(data);
        })
        .catch(() => {
          toast("Couldn't reach the server — check your connection and try again.", true);
          deleteBtn.disabled = false;
        });
    });
  }

  // Over-budget warning modal: index.php only renders #budgetWarningModal
  // into the page (includes/layout/budget_warning_modal.php) when the
  // current month is actually over budget, so showing it here is a no-op
  // guard away from needing any budget math on the JS side.
  function initBudgetWarningModal() {
    const modalEl = document.getElementById('budgetWarningModal');
    if (!modalEl) {
      return;
    }
    bootstrap.Modal.getOrCreateInstance(modalEl).show();
  }

  /** One delegated listener (works for rows rendered now or added later via innerHTML), routed by data-kind. */
  function initRowClickDelegation() {
    function handleActivate(row) {
      if (row.dataset.editable === '0') {
        return;
      }
      if (row.dataset.kind === 'income') {
        if (openIncomeEditModalFn) {
          openIncomeEditModalFn(row);
        }
      } else if (openExpenseEditModal) {
        openExpenseEditModal(row);
      }
    }

    document.addEventListener('click', (e) => {
      const row = e.target.closest('.tx');
      if (row) {
        handleActivate(row);
      }
    });
    document.addEventListener('keydown', (e) => {
      if (e.key !== 'Enter' && e.key !== ' ') {
        return;
      }
      const row = e.target.closest('.tx');
      if (row) {
        e.preventDefault();
        handleActivate(row);
      }
    });
  }

  initAddForm();
  initAddIncomeForm();
  initMonthPicker('monthPicker', 'monthPickerForm');
  initMonthPicker('incomeMonthPicker', 'incomeMonthPickerForm');
  initEditModal();
  initIncomeEditModal();
  initBudgetWarningModal();
  initRowClickDelegation();
  initVisitorTimeLocalization();
})();