function recalculate_landed_cost(frm) {
    // Recompute total_taxes_and_charges from base_amount on each tax row.
    let total = 0;
    (frm.doc.taxes || []).forEach(d => { total += flt(d.base_amount); });
    frm.doc.total_taxes_and_charges = total;
    frm.refresh_field('total_taxes_and_charges');

    const items = frm.doc.items || [];
    if (!items.length) return;

    const based_on = (frm.doc.distribute_charges_based_on || '').toLowerCase();
    // Manual mode — don't touch applicable_charges, user owns them.
    if (based_on === 'distribute manually') return;

    // Empty taxes → ERPNext's set_applicable_charges_for_item bails out and
    // leaves stale applicable_charges on items. We zero them ourselves.
    if (total === 0) {
        items.forEach(it => { it.applicable_charges = 0; });
        frm.refresh_field('items');
        return;
    }

    let total_item_cost = 0;
    items.forEach(d => { total_item_cost += flt(d[based_on]); });

    if (total_item_cost === 0) {
        items.forEach(it => { it.applicable_charges = 0; });
        frm.refresh_field('items');
        return;
    }

    let total_charges = 0;
    items.forEach(it => {
        it.applicable_charges = flt(
            (flt(it[based_on]) * total) / total_item_cost,
            precision('applicable_charges', it),
        );
        total_charges += it.applicable_charges;
    });
    // Push the rounding remainder onto the last row to keep the totals aligned.
    if (total_charges !== total) {
        items[items.length - 1].applicable_charges += (total - total_charges);
    }
    frm.refresh_field('items');
}

frappe.ui.form.on('Landed Cost Voucher', {
    setup(frm) {
        // Restrict the new `item` field on the taxes table to service items.
        frm.set_query('item', 'taxes', () => ({
            filters: { is_stock_item: 0, disabled: 0 }
        }));
    },

    get_items_from_vendor_invoices(frm) {
        // In-form button placed above the Landed Cost (taxes) table.
        // Pulls service-item rows from the Purchase Invoices listed in
        // vendor_invoices and replaces the current taxes rows.
        const invoices = (frm.doc.vendor_invoices || [])
            .map(r => r.vendor_invoice)
            .filter(Boolean);

        if (!invoices.length) {
            frappe.msgprint(__('Please add Vendor Invoices first.'));
            return;
        }
        if (!frm.doc.company) {
            frappe.msgprint(__('Please set Company first.'));
            return;
        }

        frappe.call({
            method: 'jey_erp.custom.landed_cost_voucher.get_taxes_from_vendor_invoices',
            args: { vendor_invoices: invoices, company: frm.doc.company },
            freeze: true,
            freeze_message: __('Fetching services from Vendor Invoices...'),
            callback: (r) => {
                const data = r.message || [];
                if (!data.length) {
                    frappe.msgprint(__('No service items found in the selected Vendor Invoices.'));
                    return;
                }
                frm.clear_table('taxes');
                data.forEach(row => {
                    const child = frm.add_child('taxes');
                    Object.assign(child, row);
                });
                frm.refresh_field('taxes');
                recalculate_landed_cost(frm);
            },
        });
    },
});

// `<tablefield>_remove` fires with the CHILD doctype as target (see
// frappe/public/js/frappe/form/grid_row.js → script_manager.trigger), so
// the handler must live on the child doctype, not the parent. ERPNext's
// own items_remove() on the LCV class is a no-op because it calls a
// non-existent `this.trigger`, leaving applicable_charges stale.
frappe.ui.form.on('Landed Cost Item', {
    items_remove(frm) {
        recalculate_landed_cost(frm);
    },
});

frappe.ui.form.on('Landed Cost Taxes and Charges', {
    taxes_remove(frm) {
        recalculate_landed_cost(frm);
    },

    item(frm, cdt, cdn) {
        const row = locals[cdt][cdn];
        if (!row.item) return;

        frappe.db.get_value('Item', row.item, [
            'item_name', 'is_stock_item', 'default_landed_cost_account',
        ]).then(({ message }) => {
            if (!message) return;
            if (cint(message.is_stock_item) === 1) {
                frappe.msgprint(__('Item {0} is not a service.', [row.item]));
                frappe.model.set_value(cdt, cdn, 'item', null);
                return;
            }
            // Description is hidden in the UI but still required by core
            // server logic, so we always sync it from item_name.
            if (message.item_name) {
                frappe.model.set_value(cdt, cdn, 'description', message.item_name);
            }
            // expense_account is driven solely by Item.default_landed_cost_account
            // — overwrite whatever ERPNext auto-populated (item-master/company
            // default) so the account is always controlled from the Item master.
            frappe.model.set_value(
                cdt, cdn, 'expense_account',
                message.default_landed_cost_account || null,
            );
        });
    },
});
