{{define "title"}}QFS Конфигуратор{{end}}

{{define "content"}}
<div class="space-y-4">
    <!-- Header with config name and back button -->
    <div class="flex items-center justify-between">
        <div class="flex items-center space-x-4">
            <a href="/projects" class="text-gray-500 hover:text-gray-700" title="Все проекты">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9.75L12 3l9 6.75v9A2.25 2.25 0 0118.75 21h-13.5A2.25 2.25 0 013 18.75v-9z"></path>
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 21v-6h6v6"></path>
                </svg>
            </a>
            <div class="text-2xl font-bold flex items-center gap-2" id="config-breadcrumbs">
                <a id="breadcrumb-project-code-link" href="/projects" class="text-blue-700 hover:underline">
                    <span id="breadcrumb-project-code">—</span>
                </a>
                <span class="text-gray-400">-</span>
                <a id="breadcrumb-project-variant-link" href="/projects" class="text-blue-700 hover:underline">
                    <span id="breadcrumb-project-variant">main</span>
                </a>
                <span class="text-gray-400">-</span>
                <a id="breadcrumb-config-name-link" href="#" class="text-blue-700 hover:underline">
                    <span id="breadcrumb-config-name">Конфигуратор</span>
                </a>
                <span class="text-gray-400">-</span>
                <span id="breadcrumb-config-version">main</span>
            </div>
        </div>
        <div id="save-buttons" class="hidden flex items-center space-x-2">
            <button id="refresh-prices-btn" onclick="refreshPrices()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
                Обновить цены
            </button>
            <button type="button"
                    onclick="openPriceSettingsModal()"
                    class="h-10 px-3 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 border border-gray-300 inline-flex items-center justify-center"
                    title="Настройки цен">
                Цены
            </button>
            <button onclick="saveConfig()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">
                Сохранить
            </button>
            <span id="price-update-date" class="text-sm text-gray-500"></span>
        </div>
    </div>

    <!-- Server count input -->
    <div class="bg-white rounded-lg shadow p-4 mb-4">
        <div class="flex items-center space-x-4">
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Количество серверов</label>
                <input type="number" id="server-count" min="1" value="1"
                       class="w-20 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
                       onchange="updateServerCount()">
            </div>
            <div class="text-sm text-gray-600">
                <div class="font-medium text-gray-700 mb-1">Прайслисты цен</div>
                <div id="pricelist-settings-summary">Estimate: авто, Склад: авто, Конкуренты: авто</div>
            </div>
            <div class="text-sm text-gray-500">
                <span id="server-count-info">Всего: <span id="total-server-count">1</span> сервер(а)</span>
            </div>
        </div>
    </div>

    <!-- Top-level tabs: Estimate | BOM | Ценообразование -->
    <div class="bg-white rounded-lg shadow mb-0">
        <nav class="flex border-b">
            <button id="top-tab-estimate" onclick="switchTopTab('estimate')"
                    class="px-5 py-3 text-sm font-semibold border-b-2 border-blue-600 text-blue-600">
                Estimate
            </button>
            <button id="top-tab-bom" onclick="switchTopTab('bom')"
                    class="px-5 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-500 hover:text-gray-700">
                BOM
            </button>
            <button id="top-tab-pricing" onclick="switchTopTab('pricing')"
                    class="px-5 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-500 hover:text-gray-700">
                Ценообразование
            </button>
        </nav>
    </div>

    <!-- Top-tab section: Estimate -->
    <div id="top-section-estimate">

    <!-- Category Tabs -->
    <div class="bg-white rounded-lg shadow">
        <div class="border-b">
            <nav class="flex overflow-x-auto" id="category-tabs">
                <button onclick="switchTab('base')" data-tab="base"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-blue-600 text-blue-600 whitespace-nowrap">
                    Base
                </button>
                <button onclick="switchTab('storage')" data-tab="storage"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
                    Storage
                </button>
                <button onclick="switchTab('pci')" data-tab="pci"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
                    PCI
                </button>
                <button onclick="switchTab('power')" data-tab="power"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
                    Power
                </button>
                <button onclick="switchTab('accessories')" data-tab="accessories"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
                    Accessories
                </button>
                <button onclick="switchTab('sw')" data-tab="sw"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
                    SW
                </button>
                <button onclick="switchTab('other')" data-tab="other"
                        class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
                    Other
                </button>
            </nav>
        </div>

        <!-- Tab content -->
        <div id="tab-content" class="p-4">
            <div class="text-center py-8 text-gray-500">Загрузка...</div>
        </div>
    </div>

    <!-- Cart summary -->
    <div id="cart-summary" class="bg-white rounded-lg shadow overflow-hidden">
        <button type="button"
                onclick="toggleCartSummarySection()"
                class="w-full px-4 py-3 flex items-center justify-between text-blue-900 bg-gradient-to-r from-blue-100 to-blue-50 hover:from-blue-200 hover:to-blue-100 border-b border-blue-200">
            <span class="font-semibold">Итого конфигурация</span>
            <svg id="cart-summary-toggle-icon" class="w-5 h-5 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
            </svg>
        </button>
        <div id="cart-summary-content" class="p-4">
            <div id="article-display" class="text-sm text-gray-700 mb-3 font-mono"></div>
            <div id="cart-items" class="space-y-2 mb-4"></div>
            <div class="border-t pt-3 flex justify-between items-center">
                <div class="text-lg font-bold">
                    Итого: <span id="cart-total">$0.00</span>
                </div>
                <button onclick="exportCSV()" class="px-3 py-1 bg-gray-200 text-gray-700 rounded text-sm hover:bg-gray-300">Экспорт CSV</button>
            </div>
        </div>
    </div>

    <!-- hidden inputs kept for JS compatibility -->
    <input type="hidden" id="custom-price-input" value="">
    <div id="adjusted-prices" class="hidden"></div>
    <div id="discount-info" class="hidden"></div>
    <div id="sale-prices" class="hidden"></div>

    </div><!-- end top-section-estimate -->

    <!-- Top-tab section: BOM -->
    <div id="top-section-bom" class="hidden">
        <div class="bg-white rounded-lg shadow p-4">
            <div class="mb-3">
                <p class="text-sm font-medium text-gray-700 mb-2">Вставьте таблицу из Excel (Ctrl+V в область ниже)</p>
                <div class="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-500 space-y-1">
                    <p class="font-medium text-gray-600">После вставки выберите тип для каждого столбца: P/N, Кол-во, Цена, Описание или «Не использовать».</p>
                    <p>Цена поддерживает форматы: <span class="font-mono">$5114,00</span> · <span class="font-mono">5 114.00</span> · <span class="font-mono">5114</span></p>
                    <p class="text-gray-400">Строку заголовка можно игнорировать или удалить. LOT сопоставляется автоматически по колонке P/N.</p>
                </div>
            </div>
            <div id="bom-paste-area"
                 contenteditable="true"
                 tabindex="0"
                 class="border-2 border-dashed border-gray-300 rounded-lg p-4 min-h-16 text-gray-400 focus:outline-none focus:border-blue-400 cursor-text mb-4"
                 onpaste="handleBOMPaste(event)"
                 placeholder="Нажмите сюда и вставьте из Excel (Ctrl+V)...">
                Нажмите сюда и вставьте из Excel (Ctrl+V)...
            </div>

            <!-- BOM table -->
            <div id="bom-table-container" class="hidden">
                <div class="overflow-x-auto">
                    <table class="w-full text-sm border-collapse">
                        <thead id="bom-table-head" class="bg-gray-50 text-gray-700"></thead>
                        <tbody id="bom-table-body"></tbody>
                    </table>
                </div>
                <div id="bom-errors" class="mt-2 text-xs text-red-600 hidden"></div>
                <div class="mt-3 flex items-center justify-between text-sm text-gray-600">
                    <div id="bom-stats"></div>
                    <div class="flex gap-2">
                        <button onclick="clearBOM()" class="px-3 py-1 bg-gray-100 text-gray-600 rounded hover:bg-gray-200 border border-gray-300">
                            Очистить
                        </button>
                        <button onclick="saveBOM()" class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">
                            Сохранить BOM
                        </button>
                        <button onclick="applyBOMToEstimate()" class="px-3 py-1 bg-orange-600 text-white rounded hover:bg-orange-700">
                            Перенести в эстимейт
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div><!-- end top-section-bom -->

    <!-- Top-tab section: Ценообразование -->
    <div id="top-section-pricing" class="hidden space-y-6">

        <!-- === Цена покупки === -->
        <div class="bg-white rounded-lg shadow p-4">
            <div class="flex items-baseline gap-3 mb-3">
                <h3 class="text-base font-semibold text-gray-800">Цена покупки</h3>
                <span class="text-xs text-gray-400">Цены указаны за 1 шт.</span>
            </div>
            <div class="overflow-x-auto">
                <table class="w-full text-sm border-collapse">
                    <thead class="bg-gray-50 text-gray-700">
                        <tr>
                            <th class="px-3 py-2 text-left border-b">PN вендора</th>
                            <th class="px-3 py-2 text-left border-b">Описание</th>
                            <th class="px-3 py-2 text-left border-b">LOT</th>
                            <th class="px-3 py-2 text-right border-b">Кол-во</th>
                            <th class="px-3 py-2 text-right border-b">Estimate</th>
                            <th class="px-3 py-2 text-right border-b">Склад</th>
                            <th class="px-3 py-2 text-right border-b">Конкуренты</th>
                            <th class="px-3 py-2 text-right border-b">Ручная цена</th>
                        </tr>
                    </thead>
                    <tbody id="pricing-body-buy">
                        <tr><td colspan="8" class="px-3 py-8 text-center text-gray-400">Загрузите BOM во вкладке «BOM»</td></tr>
                    </tbody>
                    <tfoot id="pricing-foot-buy" class="hidden bg-gray-50 font-semibold">
                        <tr>
                            <td colspan="4" class="px-3 py-2 text-right">Итого:</td>
                            <td class="px-3 py-2 text-right" id="pricing-total-buy-estimate">—</td>
                            <td class="px-3 py-2 text-right" id="pricing-total-buy-warehouse">—</td>
                            <td class="px-3 py-2 text-right" id="pricing-total-buy-competitor">—</td>
                            <td class="px-3 py-2 text-right font-bold" id="pricing-total-buy-vendor">—</td>
                        </tr>
                    </tfoot>
                </table>
            </div>
            <div class="mt-4 flex flex-wrap items-center gap-4">
                <label class="text-sm font-medium text-gray-700">Ручная цена:</label>
                <input type="number" id="pricing-custom-price-buy" step="0.01" min="0" placeholder="0.00"
                       class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
                       oninput="onBuyCustomPriceInput()">
                <button onclick="setPricingCustomPriceFromVendor()" class="px-3 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 border border-gray-300 text-sm">
                    BOM Цена
                </button>
                <button onclick="exportPricingCSV('buy')" class="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm">
                    Экспорт CSV
                </button>
            </div>
        </div>

        <!-- === Цена продажи === -->
        <div class="bg-white rounded-lg shadow p-4">
            <div class="flex items-baseline gap-3 mb-1">
                <h3 class="text-base font-semibold text-gray-800">Цена продажи</h3>
                <span class="text-xs text-gray-400">Цены указаны за 1 шт.</span>
            </div>
            <p class="text-xs text-gray-500 mb-3">Склад и Конкуренты умножаются на 1,3</p>
            <div class="overflow-x-auto">
                <table class="w-full text-sm border-collapse">
                    <thead class="bg-gray-50 text-gray-700">
                        <tr>
                            <th class="px-3 py-2 text-left border-b">PN вендора</th>
                            <th class="px-3 py-2 text-left border-b">Описание</th>
                            <th class="px-3 py-2 text-left border-b">LOT</th>
                            <th class="px-3 py-2 text-right border-b">Кол-во</th>
                            <th class="px-3 py-2 text-right border-b">Estimate</th>
                            <th class="px-3 py-2 text-right border-b">Склад</th>
                            <th class="px-3 py-2 text-right border-b">Конкуренты</th>
                            <th class="px-3 py-2 text-right border-b">Ручная цена</th>
                        </tr>
                    </thead>
                    <tbody id="pricing-body-sale">
                        <tr><td colspan="8" class="px-3 py-8 text-center text-gray-400">Загрузите BOM во вкладке «BOM»</td></tr>
                    </tbody>
                    <tfoot id="pricing-foot-sale" class="hidden bg-gray-50 font-semibold">
                        <tr>
                            <td colspan="4" class="px-3 py-2 text-right">Итого:</td>
                            <td class="px-3 py-2 text-right" id="pricing-total-sale-estimate">—</td>
                            <td class="px-3 py-2 text-right" id="pricing-total-sale-warehouse">—</td>
                            <td class="px-3 py-2 text-right" id="pricing-total-sale-competitor">—</td>
                            <td class="px-3 py-2 text-right font-bold" id="pricing-total-sale-vendor">—</td>
                        </tr>
                    </tfoot>
                </table>
            </div>
            <div class="mt-4 flex flex-wrap items-center gap-4">
                <label class="text-sm font-medium text-gray-700">Аплифт к estimate:</label>
                <input type="text" id="pricing-uplift-sale" inputmode="decimal" placeholder="1,3000"
                       class="w-28 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
                       oninput="onSaleMarkupInput()">
                <label class="text-sm font-medium text-gray-700">Ручная цена:</label>
                <input type="number" id="pricing-custom-price-sale" step="0.01" min="0" placeholder="0.00"
                       class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
                       oninput="onSaleCustomPriceInput()">
                <button onclick="exportPricingCSV('sale')" class="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm">
                    Экспорт CSV
                </button>
            </div>
        </div>

    </div><!-- end top-section-pricing -->

</div>

<!-- Price settings modal -->
<div id="price-settings-modal" class="hidden fixed inset-0 z-50">
    <div class="absolute inset-0 bg-black/40" onclick="closePriceSettingsModal()"></div>
    <div class="relative max-w-xl mx-auto mt-24 bg-white rounded-lg shadow-xl border">
        <div class="px-5 py-4 border-b flex items-center justify-between">
            <h3 class="text-lg font-semibold text-gray-900">Настройки цен</h3>
            <button type="button" onclick="closePriceSettingsModal()" class="text-gray-500 hover:text-gray-700">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                </svg>
            </button>
        </div>
        <div class="px-5 py-4 space-y-4">
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Estimate</label>
                <select id="settings-pricelist-estimate" class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"></select>
            </div>
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Склад</label>
                <select id="settings-pricelist-warehouse" class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"></select>
            </div>
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Конкуренты</label>
                <select id="settings-pricelist-competitor" class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"></select>
            </div>
            <label class="flex items-center gap-2 text-sm text-gray-700">
                <input id="settings-disable-price-refresh" type="checkbox" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
                <span>Не обновлять цены</span>
            </label>
            <label class="flex items-center gap-2 text-sm text-gray-700">
                <input id="settings-only-in-stock" type="checkbox" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
                <span>Только наличие</span>
            </label>
        </div>
        <div class="px-5 py-4 border-t flex justify-end gap-2">
            <button type="button" onclick="closePriceSettingsModal()" class="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200">Отмена</button>
            <button type="button" onclick="applyPriceSettings()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Применить</button>
        </div>
    </div>
</div>

<!-- Autocomplete dropdown (shared) -->
<div id="autocomplete-dropdown" class="hidden absolute z-50 bg-white border rounded-lg shadow-lg max-h-96 overflow-y-auto w-96"></div>

<style>
.autocomplete-item {
    padding: 8px 12px;
    cursor: pointer;
    border-bottom: 1px solid #f0f0f0;
}
.autocomplete-item:hover, .autocomplete-item.selected {
    background-color: #f3f4f6;
}
.autocomplete-item:last-child {
    border-bottom: none;
}
</style>

<script>
// Tab configuration - will be populated dynamically
let TAB_CONFIG = {
    base: {
        categories: ['MB', 'CPU', 'MEM', 'ENC', 'DKC', 'CTL'],
        singleSelect: true,
        label: 'Base'
    },
    storage: {
        categories: ['RAID', 'M2', 'SSD', 'HDD', 'EDSFF', 'HHHL'],
        singleSelect: false,
        label: 'Storage',
        sections: [
            { title: 'RAID Контроллеры', categories: ['RAID'] },
            { title: 'Диски', categories: ['M2', 'SSD', 'HDD', 'EDSFF', 'HHHL'] }
        ]
    },
    pci: {
        categories: ['GPU', 'DPU', 'NIC', 'HCA', 'HBA', 'HIC'],
        singleSelect: false,
        label: 'PCI',
        sections: [
            { title: 'GPU / DPU', categories: ['GPU', 'DPU'] },
            { title: 'NIC / HCA', categories: ['NIC', 'HCA'] },
            { title: 'HBA', categories: ['HBA'] },
            { title: 'HIC', categories: ['HIC'] }
        ]
    },
    power: {
        categories: ['PS', 'PSU'],
        singleSelect: false,
        label: 'Power'
    },
    accessories: {
        categories: ['ACC', 'CARD'],
        singleSelect: false,
        label: 'Accessories'
    },
    sw: {
        categories: ['SW'],
        singleSelect: false,
        label: 'SW'
    },
    other: {
        categories: [],
        singleSelect: false,
        label: 'Other'
    }
};

let ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG)
    .flatMap(t => t.categories)
    .map(c => c.toUpperCase());

// State
let configUUID = '{{.ConfigUUID}}';
let configType = 'server';
let configName = '';
let currentVersionNo = 1;
let projectUUID = '';
let projectName = '';
let projectCode = '';
let projectVariant = '';
let projectIndexLoaded = false;
let projectByUUID = {};
let projectMainByCode = {};

async function loadProjectIndex() {
    if (projectIndexLoaded) return;
    try {
        const resp = await fetch('/api/projects/all');
        if (!resp.ok) return;
        const data = await resp.json();
        const allProjects = Array.isArray(data) ? data : (data.projects || []);
        projectByUUID = {};
        projectMainByCode = {};
        allProjects.forEach(p => {
            projectByUUID[p.uuid] = p;
            const code = (p.code || '').trim();
            const variant = (p.variant || '').trim();
            if (code && (variant === '' || variant === 'main')) {
                if (!projectMainByCode[code]) {
                    projectMainByCode[code] = p.uuid;
                }
            }
        });
        projectIndexLoaded = true;
    } catch (e) {
        // ignore
    }
}

function updateConfigBreadcrumbs() {
    const codeEl = document.getElementById('breadcrumb-project-code');
    const variantEl = document.getElementById('breadcrumb-project-variant');
    const configEl = document.getElementById('breadcrumb-config-name');
    const versionEl = document.getElementById('breadcrumb-config-version');
    const projectCodeLinkEl = document.getElementById('breadcrumb-project-code-link');
    const projectVariantLinkEl = document.getElementById('breadcrumb-project-variant-link');

    let code = 'Без проекта';
    let variant = 'main';
    if (projectUUID && projectByUUID[projectUUID]) {
        code = (projectByUUID[projectUUID].code || '').trim() || 'Без проекта';
        const rawVariant = (projectByUUID[projectUUID].variant || '').trim();
        variant = rawVariant === '' ? 'main' : rawVariant;
        if (projectCodeLinkEl) {
            const mainUUID = projectMainByCode[code];
            projectCodeLinkEl.href = mainUUID ? ('/projects/' + mainUUID) : ('/projects/' + projectUUID);
        }
        if (projectVariantLinkEl) {
            projectVariantLinkEl.href = '/projects/' + projectUUID;
        }
    } else {
        if (projectCodeLinkEl) projectCodeLinkEl.href = '/projects';
        if (projectVariantLinkEl) projectVariantLinkEl.href = '/projects';
    }
    codeEl.textContent = code;
    variantEl.textContent = variant;
    const fullConfigName = configName || 'Конфигурация';
    configEl.textContent = truncateBreadcrumbSpecName(fullConfigName);
    configEl.title = fullConfigName;
    versionEl.textContent = 'main';
    document.title = code + ' / ' + variant + ' / ' + fullConfigName + ' — QFS';
    const configNameLinkEl = document.getElementById('breadcrumb-config-name-link');
    if (configNameLinkEl && configUUID) {
        configNameLinkEl.href = '/configs/' + configUUID + '/revisions';
    }
}

function truncateBreadcrumbSpecName(name) {
    const maxLength = 16;
    if (!name || name.length <= maxLength) return name;
    return name.slice(0, maxLength - 1) + '…';
}
let currentTab = 'base';
let allComponents = [];
let cart = [];
let categoryOrderMap = {}; // Category code -> display_order mapping
let configTypeCategoryMap = {}; // configTypeCode → Set<UPPER_CODE> of allowed categories (from server)
let alwaysVisibleTabsSet = null; // Set<tabKey> — null means use hardcoded fallback
let requiredCategoriesMap = {}; // configTypeCode → Set<UPPER_CODE> of required categories
let autoSaveTimeout = null; // Timeout for debounced autosave
let hasUnsavedChanges = false;
let exitSaveStarted = false;
let serverCount = 1; // Server count for the configuration
let serverModelForQuote = '';
let supportCode = '';
let currentArticle = '';
let articlePreviewTimeout = null;
let selectedPricelistIds = {
    estimate: null,
    warehouse: null,
    competitor: null
};
let resolvedAutoPricelistIds = {
    estimate: null,
    warehouse: null,
    competitor: null
};
let disablePriceRefresh = false;
let onlyInStock = false;
let activePricelistsBySource = {
    estimate: [],
    warehouse: [],
    competitor: []
};
let activePricelistsLoadedAt = 0;
let activePricelistsLoadPromise = null;
let priceLevelsRequestSeq = 0;
let priceLevelsRefreshTimer = null;
let warehouseStockLotsByPricelist = new Map();
let warehouseStockLoadSeq = 0;
let warehouseStockLoadsByPricelist = new Map();
let componentPricesCache = {}; // { lot_name: price } - caches prices loaded via API
let componentPricesCacheLoading = new Map(); // { category: Promise } - tracks ongoing price loads

// Autocomplete state
let autocompleteInput = null;
let autocompleteCategory = null;
let autocompleteMode = null; // 'single', 'multi', 'section', 'edit-item'
let autocompleteEditCategories = null;
let autocompleteIndex = -1;
let autocompleteFiltered = [];

function getDisplayPrice(item) {
    if (typeof item.unit_price === 'number' && item.unit_price > 0) {
        return item.unit_price;
    }
    if (typeof item.estimate_price === 'number' && item.estimate_price > 0) {
        return item.estimate_price;
    }
    return 0;
}

function formatNumberRu(value) {
    const rounded = Math.round(value);
    return rounded
        .toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 0 })
        .replace(/[\u202f\u00a0 ]/g, '\u00A0');
}

function formatMoney(value) {
    return '$\u00A0' + formatNumberRu(value);
}

function formatPriceOrNA(value) {
    if (typeof value !== 'number' || value <= 0) {
        return 'N/A';
    }
    return formatMoney(value);
}

function formatDelta(abs, pct) {
    if (typeof abs !== 'number') {
        return 'N/A';
    }
    const sign = abs > 0 ? '+' : abs < 0 ? '-' : '';
    const absValue = Math.abs(abs);
    if (typeof pct !== 'number') {
        return sign + formatMoney(absValue);
    }
    const pctSign = pct > 0 ? '+' : pct < 0 ? '-' : '';
    return sign + formatMoney(absValue) + ' (' + pctSign + Math.round(Math.abs(pct)) + '%)';
}

function getEffectivePricelistID(source) {
    const explicit = selectedPricelistIds[source];
    if (Number.isFinite(explicit) && explicit > 0) {
        return Number(explicit);
    }
    const resolvedAuto = resolvedAutoPricelistIds[source];
    if (Number.isFinite(resolvedAuto) && resolvedAuto > 0) {
        return Number(resolvedAuto);
    }
    const fallback = activePricelistsBySource[source]?.[0]?.id;
    if (Number.isFinite(fallback) && fallback > 0) {
        return Number(fallback);
    }
    return null;
}

async function refreshPriceLevels(options = {}) {
    const force = options.force === true;
    const noCache = options.noCache === true;
    if (!configUUID || cart.length === 0 || (disablePriceRefresh && !force)) {
        return;
    }

    const seq = ++priceLevelsRequestSeq;
    try {
        const payload = {
            items: cart.map(item => ({
                lot_name: item.lot_name,
                quantity: item.quantity
            })),
            no_cache: noCache,
            pricelist_ids: Object.fromEntries(
                Object.entries(selectedPricelistIds)
                    .filter(([, id]) => typeof id === 'number' && id > 0)
            )
        };
        const resp = await fetch('/api/quote/price-levels', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });
        if (!resp.ok) {
            return;
        }
        const data = await resp.json();
        if (seq !== priceLevelsRequestSeq) {
            return;
        }
        const byLot = new Map((data.items || []).map(i => [i.lot_name, i]));
        cart = cart.map(item => {
            const levels = byLot.get(item.lot_name);
            if (!levels) return item;
            const next = { ...item, ...levels };
            if (typeof levels.estimate_price === 'number' && levels.estimate_price > 0) {
                next.unit_price = levels.estimate_price;
            }
            return next;
        });
        if (data.resolved_pricelist_ids) {
            ['estimate', 'warehouse', 'competitor'].forEach(source => {
                if (!selectedPricelistIds[source] && data.resolved_pricelist_ids[source]) {
                    resolvedAutoPricelistIds[source] = Number(data.resolved_pricelist_ids[source]);
                }
            });
            renderPricelistSettingsSummary();
        }
    } catch(e) {
        console.error('Failed to refresh price levels', e);
    }
}

function schedulePriceLevelsRefresh(options = {}) {
    const delay = Number.isFinite(options.delay) ? options.delay : 120;
    const rerender = options.rerender !== false;
    const autosave = options.autosave === true;
    const noCache = options.noCache === true;
    const force = options.force === true;

    if (priceLevelsRefreshTimer) {
        clearTimeout(priceLevelsRefreshTimer);
        priceLevelsRefreshTimer = null;
    }

    priceLevelsRefreshTimer = setTimeout(async () => {
        priceLevelsRefreshTimer = null;
        await refreshPriceLevels({ noCache, force });
        if (rerender) {
            renderTab();
            updateCartUI();
        }
        if (autosave) {
            triggerAutoSave();
        }
    }, Math.max(0, delay));
}

function currentWarehousePricelistID() {
    return getEffectivePricelistID('warehouse');
}

async function loadWarehouseInStockLots() {
    const pricelistID = currentWarehousePricelistID();
    if (!pricelistID) return new Set();
    if (warehouseStockLotsByPricelist.has(pricelistID)) {
        return warehouseStockLotsByPricelist.get(pricelistID);
    }
    const existingLoad = warehouseStockLoadsByPricelist.get(pricelistID);
    if (existingLoad) {
        return existingLoad;
    }

    const loadPromise = (async () => {
        const seq = ++warehouseStockLoadSeq;
        const result = new Set();
        const resp = await fetch(`/api/pricelists/${pricelistID}/lots`);
        if (!resp.ok) {
            throw new Error(`warehouse lots request failed: ${resp.status}`);
        }
        const data = await resp.json();
        const lotNames = Array.isArray(data.lot_names) ? data.lot_names : [];
        lotNames.forEach(lot => {
            if (typeof lot === 'string' && lot.trim() !== '') {
                result.add(lot);
            }
        });

        if (seq === warehouseStockLoadSeq) {
            warehouseStockLotsByPricelist.set(pricelistID, result);
        }
        return result;
    })();

    warehouseStockLoadsByPricelist.set(pricelistID, loadPromise);
    try {
        return await loadPromise;
    } finally {
        warehouseStockLoadsByPricelist.delete(pricelistID);
    }
}

async function ensureWarehouseStockFilterLoaded() {
    if (!onlyInStock) return;
    try {
        await loadWarehouseInStockLots();
    } catch (e) {
        console.error('Failed to load warehouse availability filter', e);
        showToast('Не удалось загрузить наличие склада', 'error');
    }
}

function isComponentAllowedByStockFilter(comp) {
    if (!onlyInStock) return true;
    const pricelistID = currentWarehousePricelistID();
    if (!pricelistID) return false;
    const availableLots = warehouseStockLotsByPricelist.get(pricelistID);
    // Don't block UI while stock set is being loaded.
    if (!availableLots) return true;
    return availableLots.has(comp.lot_name);
}

// Load categories from API and update tab configuration
async function loadCategoriesFromAPI() {
    try {
        const resp = await fetch('/api/categories');
        const cats = await resp.json();

        // Build category order map
        categoryOrderMap = {};
        cats.forEach(cat => {
            categoryOrderMap[cat.code.toUpperCase()] = cat.display_order;
        });

        // Build list of unassigned categories
        const knownCodes = Object.values(TAB_CONFIG)
            .flatMap(t => t.categories)
            .map(c => c.toUpperCase());

        const unassignedCategories = cats
            .filter(cat => !knownCodes.includes(cat.code.toUpperCase()))
            .sort((a, b) => a.display_order - b.display_order)
            .map(cat => cat.code);

        // Update "other" tab with unassigned categories
        TAB_CONFIG.other.categories = unassignedCategories;

        // Rebuild ASSIGNED_CATEGORIES
        ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG)
            .flatMap(t => t.categories)
            .map(c => c.toUpperCase());
    } catch(e) {
        console.error('Failed to load categories, using defaults', e);
        // Will use default configuration if API fails
    }
}

async function loadCfgSettings() {
    if (typeof _cfgSettings !== 'undefined' && _cfgSettings) return _cfgSettings;
    try {
        const r = await fetch('/api/configurator-settings');
        if (r.ok) {
            window._cfgSettings = await r.json();
            return window._cfgSettings;
        }
    } catch(e) { /* fallback to hardcoded */ }
    return null;
}

function applyServerSettings(settings) {
    if (!settings) return;

    // config_types → category allowlist map
    if (Array.isArray(settings.config_types) && settings.config_types.length) {
        configTypeCategoryMap = {};
        settings.config_types.forEach(ct => {
            if (ct.code && Array.isArray(ct.categories)) {
                configTypeCategoryMap[ct.code] = new Set(ct.categories.map(c => c.toUpperCase()));
            }
        });
    }

    // tab_config → update TAB_CONFIG (preserve .other)
    if (Array.isArray(settings.tab_config) && settings.tab_config.length) {
        const otherTab = TAB_CONFIG.other;
        TAB_CONFIG = {};
        settings.tab_config.forEach(tab => {
            TAB_CONFIG[tab.key] = {
                categories: Array.isArray(tab.categories) ? tab.categories : [],
                singleSelect: !!tab.single_select,
                label: tab.label || tab.key,
                sections: tab.sections || undefined
            };
        });
        TAB_CONFIG.other = otherTab || { categories: [], singleSelect: false, label: 'Other' };
        ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG).flatMap(t => t.categories).map(c => c.toUpperCase());
    }

    // always_visible_tabs
    if (Array.isArray(settings.always_visible_tabs) && settings.always_visible_tabs.length) {
        alwaysVisibleTabsSet = new Set(settings.always_visible_tabs);
    }

    // required_categories
    if (settings.required_categories && typeof settings.required_categories === 'object') {
        requiredCategoriesMap = {};
        Object.entries(settings.required_categories).forEach(([ct, codes]) => {
            if (Array.isArray(codes)) {
                requiredCategoriesMap[ct] = new Set(codes.map(c => c.toUpperCase()));
            }
        });
    }

    applyConfigTypeToTabs();
    updateTabVisibility();
    updateRequiredCategoryBadges();
}

function updateRequiredCategoryBadges() {
    const required = requiredCategoriesMap[configType];
    if (!required || !required.size) return;

    // Build set of categories that have at least one cart item
    const filledCategories = new Set(
        cart.map(item => (item.category || getCategoryFromLotName(item.lot_name) || '').toUpperCase())
    );

    // For each tab, check if it contains any required-but-unfilled category
    Object.entries(TAB_CONFIG).forEach(([tabKey, tabCfg]) => {
        const btn = document.querySelector(`[data-tab="${tabKey}"]`);
        if (!btn) return;
        const tabCategories = (tabCfg.categories || []).map(c => c.toUpperCase());
        const hasUnfilled = tabCategories.some(cat => required.has(cat) && !filledCategories.has(cat));
        const badge = btn.querySelector('.required-badge');
        if (hasUnfilled) {
            if (!badge) {
                const dot = document.createElement('span');
                dot.className = 'required-badge inline-block w-1.5 h-1.5 bg-orange-400 rounded-full ml-1 align-middle';
                btn.appendChild(dot);
            }
        } else if (badge) {
            badge.remove();
        }
    });
}

// Initialize
document.addEventListener('DOMContentLoaded', async function() {
    // RBAC disabled - no token check required
    if (!configUUID) {
        window.location.href = '/configs';
        return;
    }

    // Load categories and configurator settings in background (defaults are usable immediately).
    const categoriesPromise = loadCategoriesFromAPI().catch(() => {});
    loadCfgSettings().then(s => applyServerSettings(s)).catch(() => {});

    try {
        const resp = await fetch('/api/configs/' + configUUID);

        if (resp.status === 404) {
            showToast('Конфигурация не найдена', 'error');
            window.location.href = '/configs';
            return;
        }

        const config = await resp.json();
        configName = config.name;
        configType = config.config_type || 'server';
        applyConfigTypeToTabs();
        currentVersionNo = config.current_version_no || 1;
        projectUUID = config.project_uuid || '';
        await loadProjectIndex();
        updateConfigBreadcrumbs();
        document.getElementById('save-buttons').classList.remove('hidden');

        // Set server count from config
        serverCount = config.server_count || 1;
        document.getElementById('server-count').value = serverCount;
        document.getElementById('total-server-count').textContent = serverCount;
        selectedPricelistIds.estimate = config.pricelist_id || null;
        selectedPricelistIds.warehouse = config.warehouse_pricelist_id || null;
        selectedPricelistIds.competitor = config.competitor_pricelist_id || null;
        disablePriceRefresh = Boolean(config.disable_price_refresh);
        onlyInStock = Boolean(config.only_in_stock);

        if (config.items && config.items.length > 0) {
            cart = config.items.map(item => ({
                lot_name: item.lot_name,
                quantity: item.quantity,
                unit_price: item.unit_price,
                estimate_price: item.unit_price,
                warehouse_price: null,
                competitor_price: null,
                description: item.description || '',
                category: item.category || getCategoryFromLotName(item.lot_name)
            }));
        }
        serverModelForQuote = config.server_model || '';
        supportCode = config.support_code || '';
        currentArticle = config.article || '';
        restorePricingStateFromNotes(config.notes || '');

        // Restore custom price if saved
        if (config.custom_price) {
            document.getElementById('custom-price-input').value = config.custom_price.toFixed(2);
        }

        // Display price update date if available
        if (config.price_updated_at) {
            updatePriceUpdateDate(config.price_updated_at);
        }
    } catch(e) {
        showToast('Ошибка загрузки конфигурации', 'error');
        window.location.href = '/configs';
        return;
    }

    restoreLocalPriceSettings();
    await Promise.all([
        loadActivePricelists(),
        loadAllComponents(),
        categoriesPromise,
    ]);
    syncPriceSettingsControls();
    renderPricelistSettingsSummary();
    updateRefreshPricesButtonState();
    renderTab();
    updateCartUI();
    ensureWarehouseStockFilterLoaded().then(() => {
        renderTab();
        updateCartUI();
    });
    schedulePriceLevelsRefresh({ delay: 0, rerender: true, autosave: false });

    // Close autocomplete on outside click
    document.addEventListener('click', function(e) {
        if (!e.target.closest('.autocomplete-wrapper')) {
            hideAutocomplete();
        }
    });

    // Save pricing state (ручная цена) on page exit so it survives navigation
    window.addEventListener('pagehide', saveConfigOnExit);
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') saveConfigOnExit();
    });

    // Load vendor spec BOM for this configuration
    if (configUUID) {
        loadVendorSpec(configUUID);
    }
});

async function loadAllComponents() {
    try {
        const resp = await fetch('/api/components?per_page=5000');
        const data = await resp.json();
        allComponents = data.items || [];
        window._bomAllComponents = allComponents;
    } catch(e) {
        console.error('Failed to load components', e);
        allComponents = [];
        window._bomAllComponents = [];
    }
}
function _bomLots() {
    return [...new Set((window._bomAllComponents || allComponents).map(c => c.lot_name).filter(Boolean))].sort();
}
const BOM_LOT_DATALIST_DIVIDER = '────────';
function _bomLotValid(v) {
    const lot = (v || '').trim();
    if (!lot || lot === BOM_LOT_DATALIST_DIVIDER) return false;
    return (window._bomAllComponents || allComponents).some(c => c.lot_name === lot);
}

function updateServerCount() {
    const serverCountInput = document.getElementById('server-count');
    const newCount = parseInt(serverCountInput.value) || 1;
    serverCount = Math.max(1, newCount);
    serverCountInput.value = serverCount;

    // Update total server count display
    document.getElementById('total-server-count').textContent = serverCount;

    // Update cart UI to reflect the server count
    updateCartUI();

    // Trigger auto-save
    triggerAutoSave();
}

async function loadActivePricelists(force = false) {
    const now = Date.now();
    const isFresh = (now - activePricelistsLoadedAt) < 15000;
    if (!force && isFresh) {
        return;
    }
    if (activePricelistsLoadPromise) {
        await activePricelistsLoadPromise;
        return;
    }

    const sources = ['estimate', 'warehouse', 'competitor'];
    activePricelistsLoadPromise = (async () => {
        await Promise.all(sources.map(async source => {
            try {
                const resp = await fetch(`/api/pricelists?active_only=true&source=${source}&per_page=200`);
                const data = await resp.json();
                activePricelistsBySource[source] = data.items || [];
                // Do not reset the stored pricelist — it may be inactive but must be preserved
            } catch (e) {
                activePricelistsBySource[source] = [];
            }
        }));
        activePricelistsLoadedAt = Date.now();
    })();

    try {
        await activePricelistsLoadPromise;
    } finally {
        activePricelistsLoadPromise = null;
    }
}

function renderPricelistSelectOptions(selectId, source) {
    const select = document.getElementById(selectId);
    if (!select) return;
    const pricelists = activePricelistsBySource[source] || [];
    if (pricelists.length === 0) {
        select.innerHTML = '<option value="">Нет активных прайслистов</option>';
        select.value = '';
        return;
    }
    select.innerHTML = pricelists.map(pl => {
        return `<option value="${pl.id}">${escapeHtml(pl.version)}</option>`;
    }).join('');
    const current = selectedPricelistIds[source];
    if (current) {
        select.value = String(current);
        // Stored pricelist may be inactive — add it as a virtual option if not found
        if (!select.value) {
            const opt = document.createElement('option');
            opt.value = String(current);
            opt.textContent = `ID ${current} (неактивный)`;
            select.prepend(opt);
            select.value = String(current);
        }
    } else if (pricelists.length > 0) {
        // New config: pre-select the first (latest) pricelist
        selectedPricelistIds[source] = Number(pricelists[0].id);
        select.value = String(pricelists[0].id);
    }
}

function syncPriceSettingsControls() {
    renderPricelistSelectOptions('settings-pricelist-estimate', 'estimate');
    renderPricelistSelectOptions('settings-pricelist-warehouse', 'warehouse');
    renderPricelistSelectOptions('settings-pricelist-competitor', 'competitor');
    const disableCheckbox = document.getElementById('settings-disable-price-refresh');
    if (disableCheckbox) {
        disableCheckbox.checked = disablePriceRefresh;
    }
    const inStockCheckbox = document.getElementById('settings-only-in-stock');
    if (inStockCheckbox) {
        inStockCheckbox.checked = onlyInStock;
    }
}

function getPricelistVersionById(source, id) {
    const pricelists = activePricelistsBySource[source] || [];
    const found = pricelists.find(pl => Number(pl.id) === Number(id));
    return found ? found.version : null;
}

function renderPricelistSettingsSummary() {
    const summary = document.getElementById('pricelist-settings-summary');
    if (!summary) return;
    const estimate = selectedPricelistIds.estimate ? getPricelistVersionById('estimate', selectedPricelistIds.estimate) || `ID ${selectedPricelistIds.estimate}` : '—';
    const warehouse = selectedPricelistIds.warehouse ? getPricelistVersionById('warehouse', selectedPricelistIds.warehouse) || `ID ${selectedPricelistIds.warehouse}` : '—';
    const competitor = selectedPricelistIds.competitor ? getPricelistVersionById('competitor', selectedPricelistIds.competitor) || `ID ${selectedPricelistIds.competitor}` : '—';
    const refreshState = disablePriceRefresh ? ' | Обновление цен: выкл' : '';
    const stockFilterState = onlyInStock ? ' | Только наличие: вкл' : '';
    summary.textContent = `Estimate: ${estimate}, Склад: ${warehouse}, Конкуренты: ${competitor}${refreshState}${stockFilterState}`;
}

function updateRefreshPricesButtonState() {
    const refreshBtn = document.getElementById('refresh-prices-btn');
    if (!refreshBtn) return;
    if (disablePriceRefresh) {
        refreshBtn.disabled = true;
        refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
        refreshBtn.title = 'Обновление цен отключено в настройках';
    } else {
        refreshBtn.disabled = false;
        refreshBtn.className = 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700';
        refreshBtn.title = '';
    }
}

function getPriceSettingsStorageKey() {
    return `qf_price_settings_${configUUID || 'default'}`;
}

function persistLocalPriceSettings() {
    try {
        localStorage.setItem(getPriceSettingsStorageKey(), JSON.stringify({
            pricelist_ids: selectedPricelistIds,
            disable_price_refresh: disablePriceRefresh
        }));
    } catch (e) {
        // ignore localStorage failures
    }
}

function restoreLocalPriceSettings() {
    try {
        const raw = localStorage.getItem(getPriceSettingsStorageKey());
        if (!raw) return;
        const parsed = JSON.parse(raw);
        if (parsed && parsed.pricelist_ids) {
            ['estimate', 'warehouse', 'competitor'].forEach(source => {
                const next = parseInt(parsed.pricelist_ids[source]);
                if (Number.isFinite(next) && next > 0) {
                    selectedPricelistIds[source] = next;
                }
            });
        }
        disablePriceRefresh = Boolean(parsed?.disable_price_refresh);
    } catch (e) {
        // ignore invalid localStorage payload
    }
}

function openPriceSettingsModal() {
    syncPriceSettingsControls();
    renderPricelistSettingsSummary();
    document.getElementById('price-settings-modal')?.classList.remove('hidden');
    loadActivePricelists().then(() => {
        syncPriceSettingsControls();
        renderPricelistSettingsSummary();
    });
}

function closePriceSettingsModal() {
    document.getElementById('price-settings-modal')?.classList.add('hidden');
}

function applyPriceSettings() {
    const estimateVal = parseInt(document.getElementById('settings-pricelist-estimate')?.value || '');
    const warehouseVal = parseInt(document.getElementById('settings-pricelist-warehouse')?.value || '');
    const competitorVal = parseInt(document.getElementById('settings-pricelist-competitor')?.value || '');
    const disableVal = Boolean(document.getElementById('settings-disable-price-refresh')?.checked);
    const inStockVal = Boolean(document.getElementById('settings-only-in-stock')?.checked);

    const prevWarehouseID = currentWarehousePricelistID();
    if (Number.isFinite(estimateVal) && estimateVal > 0) {
        selectedPricelistIds.estimate = estimateVal;
        resolvedAutoPricelistIds.estimate = null;
    }
    if (Number.isFinite(warehouseVal) && warehouseVal > 0) {
        selectedPricelistIds.warehouse = warehouseVal;
        resolvedAutoPricelistIds.warehouse = null;
    }
    if (Number.isFinite(competitorVal) && competitorVal > 0) {
        selectedPricelistIds.competitor = competitorVal;
        resolvedAutoPricelistIds.competitor = null;
    }
    disablePriceRefresh = disableVal;
    onlyInStock = inStockVal;

    const nextWarehouseID = currentWarehousePricelistID();
    if (Number.isFinite(prevWarehouseID) && prevWarehouseID > 0 && prevWarehouseID !== nextWarehouseID) {
        warehouseStockLotsByPricelist.delete(prevWarehouseID);
    }
    if (onlyInStock) {
        ensureWarehouseStockFilterLoaded().then(() => {
            renderTab();
            updateCartUI();
        });
    }

    updateRefreshPricesButtonState();
    renderPricelistSettingsSummary();
    persistLocalPriceSettings();
    closePriceSettingsModal();
    schedulePriceLevelsRefresh({ delay: 0, rerender: true, autosave: true });
}

function getCategoryFromLotName(lotName) {
    const parts = lotName.split('_');
    return parts[0] || '';
}

function getComponentCategory(comp) {
    return (comp.category || getCategoryFromLotName(comp.lot_name)).toUpperCase();
}

function getTabForCategory(category) {
    const cat = category.toUpperCase();
    for (const [tabKey, tabConfig] of Object.entries(TAB_CONFIG)) {
        if (tabConfig.categories.map(c => c.toUpperCase()).includes(cat)) {
            return tabKey;
        }
    }
    return 'other';
}

function switchTab(tab) {
    currentTab = tab;
    document.querySelectorAll('.tab-btn').forEach(btn => {
        if (btn.dataset.tab === tab) {
            btn.classList.remove('border-transparent', 'text-gray-500');
            btn.classList.add('border-blue-600', 'text-blue-600');
        } else {
            btn.classList.add('border-transparent', 'text-gray-500');
            btn.classList.remove('border-blue-600', 'text-blue-600');
        }
    });
    hideAutocomplete();
    renderTab();
}

// Hardcoded fallback constants — used only when server has not provided config_types data
const ALWAYS_VISIBLE_TABS = new Set(['base', 'storage', 'pci']);
const STORAGE_ONLY_BASE_CATEGORIES = ['DKC', 'CTL', 'ENC'];
const SERVER_ONLY_BASE_CATEGORIES  = ['MB', 'CPU', 'MEM'];
const STORAGE_HIDDEN_STORAGE_CATEGORIES = ['RAID'];
const STORAGE_HIDDEN_PCI_CATEGORIES = ['GPU', 'DPU'];
const STORAGE_HIDDEN_POWER_CATEGORIES = ['PS', 'PSU'];

function isCategoryVisibleForConfigType(code, cfgType) {
    const allowed = configTypeCategoryMap[cfgType];
    if (!allowed || allowed.size === 0) return _hardcodedCategoryVisible(code, cfgType);
    return allowed.has(code.toUpperCase());
}

function _hardcodedCategoryVisible(code, cfgType) {
    if (cfgType === 'storage') {
        if (STORAGE_ONLY_BASE_CATEGORIES.includes(code)) return true;
        if (SERVER_ONLY_BASE_CATEGORIES.includes(code)) return false;
        if (STORAGE_HIDDEN_STORAGE_CATEGORIES.includes(code)) return false;
        if (STORAGE_HIDDEN_PCI_CATEGORIES.includes(code)) return false;
        if (STORAGE_HIDDEN_POWER_CATEGORIES.includes(code)) return false;
    } else {
        if (STORAGE_ONLY_BASE_CATEGORIES.includes(code)) return false;
    }
    return true;
}

function _effectiveAlwaysVisibleTabs() {
    return alwaysVisibleTabsSet || ALWAYS_VISIBLE_TABS;
}

function applyConfigTypeToTabs() {
    // Filter each tab's categories by visibility for current configType.
    // Uses server-driven allowlists when available; falls back to hardcoded constants.
    Object.keys(TAB_CONFIG).forEach(tabKey => {
        if (tabKey === 'other') return;
        const tab = TAB_CONFIG[tabKey];
        if (!tab || !Array.isArray(tab.categories)) return;

        // Snapshot the full category list for this tab (stored in _allCategories if not yet saved)
        if (!tab._allCategories) tab._allCategories = [...tab.categories];

        tab.categories = tab._allCategories.filter(c => isCategoryVisibleForConfigType(c, configType));

        if (Array.isArray(tab._allSections || tab.sections)) {
            const allSections = tab._allSections || tab.sections;
            if (!tab._allSections) tab._allSections = allSections.map(s => ({ ...s, categories: [...s.categories] }));
            tab.sections = tab._allSections
                .map(section => ({
                    ...section,
                    categories: section.categories.filter(c => isCategoryVisibleForConfigType(c, configType))
                }))
                .filter(section => section.categories.length > 0);
        }
    });

    // Rebuild assigned categories index
    ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG)
        .flatMap(t => t.categories)
        .map(c => c.toUpperCase());
}

function updateTabVisibility() {
    const visibleTabs = _effectiveAlwaysVisibleTabs();
    for (const tabId of Object.keys(TAB_CONFIG)) {
        if (visibleTabs.has(tabId)) continue;
        const btn = document.querySelector(`[data-tab="${tabId}"]`);
        if (!btn) continue;
        const hasComponents = getComponentsForTab(tabId).length > 0;
        const hasCartItems = cart.some(item => {
            const cat = (item.category || getCategoryFromLotName(item.lot_name) || '').toUpperCase();
            return getTabForCategory(cat) === tabId;
        });
        const visible = hasComponents || hasCartItems;
        btn.classList.toggle('hidden', !visible);
        // If the current tab just got hidden, fall back to base
        if (!visible && currentTab === tabId) {
            switchTab('base');
        }
    }
}

function getComponentsForTab(tab) {
    const config = TAB_CONFIG[tab];
    return allComponents.filter(comp => {
        const category = getComponentCategory(comp);
        if (tab === 'other') {
            return !ASSIGNED_CATEGORIES.includes(category);
        }
        return config.categories.map(c => c.toUpperCase()).includes(category);
    });
}

function getComponentsForCategory(category) {
    return allComponents.filter(comp => {
        return getComponentCategory(comp) === category.toUpperCase();
    });
}

function renderTab() {
    const config = TAB_CONFIG[currentTab];
    const components = getComponentsForTab(currentTab);

    if (config.singleSelect) {
        renderSingleSelectTab(config.categories);
    } else if (config.sections) {
        renderMultiSelectTabWithSections(config.sections);
    } else {
        renderMultiSelectTab(components);
    }
}

function renderSingleSelectTab(categories) {
    let html = '';
    if (currentTab === 'base') {
        html += `
        <div class="mb-1 grid grid-cols-1 md:grid-cols-[1fr,16rem] gap-3 items-start">
            <label for="server-model-input" class="block text-sm font-medium text-gray-700">Модель системы для партномера:</label>
            <label for="support-code-select" class="block text-sm font-medium text-gray-700">Уровень техподдержки:</label>
        </div>
        <div class="mb-3 grid grid-cols-1 md:grid-cols-[1fr,16rem] gap-3 items-start">
            <input type="text"
                   id="server-model-input"
                   value="${escapeHtml(serverModelForQuote)}"
                   class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
                   oninput="updateServerModelForQuote(this.value)">
            <select id="support-code-select"
                    class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
                    onchange="updateSupportCode(this.value)">
                <option value="">—</option>
                <option value="1yW" ${supportCode === '1yW' ? 'selected' : ''}>1yW</option>
                <option value="1yB" ${supportCode === '1yB' ? 'selected' : ''}>1yB</option>
                <option value="1yS" ${supportCode === '1yS' ? 'selected' : ''}>1yS</option>
                <option value="1yP" ${supportCode === '1yP' ? 'selected' : ''}>1yP</option>
            </select>
        </div>
        `;
    }
    html += `
        <table class="w-full">
            <thead class="bg-gray-50">
                <tr>
                    <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase w-24">Тип</th>
                    <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
                    <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
                    <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-24">Estimate</th>
                    <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase w-20">Кол-во</th>
                    <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Стоимость</th>
                    <th class="px-3 py-2 w-10"></th>
                </tr>
            </thead>
            <tbody class="divide-y">
    `;

    categories.forEach(cat => {
        const catLabel = cat === 'MB' ? 'MB' : cat === 'CPU' ? 'CPU' : cat === 'MEM' ? 'MEM' : cat;
        const selectedItem = cart.find(item =>
            (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase() === cat.toUpperCase()
        );

        const comp = selectedItem ? allComponents.find(c => c.lot_name === selectedItem.lot_name) : null;
        const price = comp?.current_price || 0;
        const estimate = selectedItem?.estimate_price ?? price;
        const qty = selectedItem?.quantity || 1;
        const total = (selectedItem ? getDisplayPrice(selectedItem) : price) * qty;

        html += `
            <tr class="hover:bg-gray-50">
                <td class="px-3 py-2 text-sm font-medium text-gray-700">${catLabel}</td>
                <td class="px-3 py-2">
                    <div class="autocomplete-wrapper relative">
                        <input type="text"
                               id="input-${cat}"
                               value="${selectedItem?.lot_name || ''}"
                               placeholder="Введите артикул..."
                               class="w-full px-2 py-1 border rounded text-sm font-mono"
                               onfocus="showAutocomplete('${cat}', this)"
                               oninput="filterAutocomplete('${cat}', this.value)"
                               onkeydown="handleAutocompleteKey(event, '${cat}')">
                    </div>
                </td>
                <td class="px-3 py-2 text-sm text-gray-500 truncate max-w-xs" id="desc-${cat}">${escapeHtml(comp?.description || '')}</td>
                <td class="px-3 py-2 text-sm text-right" id="price-${cat}">${formatPriceOrNA(estimate)}</td>
                <td class="px-3 py-2 text-center">
                    <input type="number" min="1" value="${qty}"
                           id="qty-${cat}"
                           onchange="updateSingleQuantity('${cat}', this.value)"
                           class="w-16 px-2 py-1 border rounded text-center text-sm">
                </td>
                <td class="px-3 py-2 text-sm text-right font-medium" id="total-${cat}">${total ? formatMoney(total) : '—'}</td>
                <td class="px-3 py-2 text-center">
                    ${selectedItem ? `
                        <button onclick="clearSingleSelect('${cat}')" class="text-red-500 hover:text-red-700">
                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                            </svg>
                        </button>
                    ` : ''}
                </td>
            </tr>
        `;
    });

    html += '</tbody></table>';
    document.getElementById('tab-content').innerHTML = html;
}

function renderMultiSelectTab(components) {
    // Get cart items for this tab
    const tabItems = cart.filter(item => {
        const cat = (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase();
        const tab = getTabForCategory(cat);
        return tab === currentTab;
    });

    let html = `
        <table class="w-full">
            <thead class="bg-gray-50">
                <tr>
                    <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
                    <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
                    <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-24">Estimate</th>
                    <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase w-20">Кол-во</th>
                    <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Стоимость</th>
                    <th class="px-3 py-2 w-10"></th>
                </tr>
            </thead>
            <tbody class="divide-y">
    `;

    // Render existing cart items for this tab
    tabItems.forEach((item, idx) => {
        const comp = allComponents.find(c => c.lot_name === item.lot_name);
        const total = getDisplayPrice(item) * item.quantity;

        html += `
            <tr class="hover:bg-gray-50">
                <td class="px-3 py-2 min-w-48">
                    <div class="autocomplete-wrapper relative">
                        <input type="text"
                               value="${escapeHtml(item.lot_name)}"
                               class="w-full px-2 py-1 border rounded text-sm font-mono"
                               onfocus="showAutocompleteEditItem('${item.lot_name}', this)"
                               oninput="filterAutocompleteEditItem(this.value)"
                               onkeydown="handleAutocompleteKeyEditItem(event)">
                    </div>
                </td>
                <td class="px-3 py-2 text-sm text-gray-500 truncate max-w-xs">${escapeHtml(item.description || comp?.description || '')}</td>
                <td class="px-3 py-2 text-sm text-right">${formatPriceOrNA(item.estimate_price ?? item.unit_price)}</td>
                <td class="px-3 py-2 text-center">
                    <input type="number" min="1" value="${item.quantity}"
                           onchange="updateMultiQuantity('${item.lot_name}', this.value)"
                           class="w-16 px-2 py-1 border rounded text-center text-sm">
                </td>
                <td class="px-3 py-2 text-sm text-right font-medium">${formatMoney(total)}</td>
                <td class="px-3 py-2 text-center">
                    <button onclick="removeFromCart('${item.lot_name}')" class="text-red-500 hover:text-red-700">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                        </svg>
                    </button>
                </td>
            </tr>
        `;
    });

    // Add empty row for new item
    html += `
        <tr class="hover:bg-gray-50 bg-gray-50">
            <td class="px-3 py-2" colspan="2">
                <div class="autocomplete-wrapper relative">
                    <input type="text"
                           id="input-new-item"
                           placeholder="Добавить компонент..."
                           class="w-full px-2 py-1 border rounded text-sm"
                           onfocus="showAutocompleteMulti(this)"
                           oninput="filterAutocompleteMulti(this.value)"
                           onkeydown="handleAutocompleteKeyMulti(event)">
                </div>
            </td>
            <td class="px-3 py-2 text-sm text-right text-gray-400" id="new-price">N/A</td>
            <td class="px-3 py-2 text-center">
                <input type="number" min="1" value="1" id="new-qty"
                       class="w-16 px-2 py-1 border rounded text-center text-sm">
            </td>
            <td class="px-3 py-2 text-sm text-right text-gray-400" id="new-total">—</td>
            <td class="px-3 py-2"></td>
        </tr>
    `;

    html += '</tbody></table>';
    html += `<p class="text-center text-sm text-gray-500 mt-4">Доступно компонентов: ${components.length}</p>`;

    document.getElementById('tab-content').innerHTML = html;
}

function renderMultiSelectTabWithSections(sections) {
    // Get cart items for this tab
    const tabItems = cart.filter(item => {
        const cat = (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase();
        const tab = getTabForCategory(cat);
        return tab === currentTab;
    });

    let html = '';
    let totalComponents = 0;

    sections.forEach((section, sectionIdx) => {
        // Get components for this section's categories
        const sectionCategories = section.categories.map(c => c.toUpperCase());
        const sectionComponents = allComponents.filter(comp => {
            const category = getComponentCategory(comp);
            return sectionCategories.includes(category);
        });
        totalComponents += sectionComponents.length;

        // Get cart items for this section
        const sectionItems = tabItems.filter(item => {
            const cat = (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase();
            return sectionCategories.includes(cat);
        });

        // Section header
        html += `
            <div class="mb-6 ${sectionIdx > 0 ? 'mt-6' : ''}">
                <h3 class="text-sm font-semibold text-gray-700 mb-3 px-3">${section.title}</h3>
                <table class="w-full">
                    <thead class="bg-gray-50">
                        <tr>
                            <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
                            <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
                            <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-24">Estimate</th>
                            <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase w-20">Кол-во</th>
                            <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Стоимость</th>
                            <th class="px-3 py-2 w-10"></th>
                        </tr>
                    </thead>
                    <tbody class="divide-y">
        `;

        // Add empty row for new item in this section
        const sectionId = section.categories.join('-');
        const categoriesStr = section.categories.join(',');

        // Render existing cart items for this section
        sectionItems.forEach((item) => {
            const comp = allComponents.find(c => c.lot_name === item.lot_name);
            const total = getDisplayPrice(item) * item.quantity;

            html += `
                <tr class="hover:bg-gray-50">
                    <td class="px-3 py-2 min-w-48">
                        <div class="autocomplete-wrapper relative">
                            <input type="text"
                                   value="${escapeHtml(item.lot_name)}"
                                   data-categories="${categoriesStr}"
                                   class="w-full px-2 py-1 border rounded text-sm font-mono"
                                   onfocus="showAutocompleteEditItem('${item.lot_name}', this)"
                                   oninput="filterAutocompleteEditItem(this.value)"
                                   onkeydown="handleAutocompleteKeyEditItem(event)">
                        </div>
                    </td>
                    <td class="px-3 py-2 text-sm text-gray-500 truncate max-w-xs">${escapeHtml(item.description || comp?.description || '')}</td>
                    <td class="px-3 py-2 text-sm text-right">${formatPriceOrNA(item.estimate_price ?? item.unit_price)}</td>
                    <td class="px-3 py-2 text-center">
                        <input type="number" min="1" value="${item.quantity}"
                               onchange="updateMultiQuantity('${item.lot_name}', this.value)"
                               class="w-16 px-2 py-1 border rounded text-center text-sm">
                    </td>
                    <td class="px-3 py-2 text-sm text-right font-medium">${formatMoney(total)}</td>
                    <td class="px-3 py-2 text-center">
                        <button onclick="removeFromCart('${item.lot_name}')" class="text-red-500 hover:text-red-700">
                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                            </svg>
                        </button>
                    </td>
                </tr>
            `;
        });

        // Add empty row for new item in this section
        html += `
            <tr class="hover:bg-gray-50 bg-gray-50">
                <td class="px-3 py-2" colspan="2">
                    <div class="autocomplete-wrapper relative">
                        <input type="text"
                               id="input-section-${sectionId}"
                               data-categories="${categoriesStr}"
                               placeholder="Добавить ${section.title.toLowerCase()}..."
                               class="w-full px-2 py-1 border rounded text-sm"
                               onfocus="showAutocompleteSection('${sectionId}', this)"
                               oninput="filterAutocompleteSection('${sectionId}', this.value, this)"
                               onkeydown="handleAutocompleteKeySection(event, '${sectionId}')">
                    </div>
                </td>
                <td class="px-3 py-2 text-sm text-right text-gray-400" id="new-price-${sectionId}">N/A</td>
                <td class="px-3 py-2 text-center">
                    <input type="number" min="1" value="1" id="new-qty-${sectionId}"
                           class="w-16 px-2 py-1 border rounded text-center text-sm">
                </td>
                <td class="px-3 py-2 text-sm text-right text-gray-400" id="new-total-${sectionId}">—</td>
                <td class="px-3 py-2"></td>
            </tr>
        `;

        html += `
                    </tbody>
                </table>
                <p class="text-center text-sm text-gray-500 mt-2">Доступно: ${sectionComponents.length}</p>
            </div>
        `;
    });

    document.getElementById('tab-content').innerHTML = html;
}

// Load prices for components in a category/tab via API
async function ensurePricesLoaded(components) {
    if (!components || components.length === 0) return;

    // Filter out components that already have prices cached
    const toLoad = components.filter(c => !(c.lot_name in componentPricesCache));
    if (toLoad.length === 0) return;

    try {
        // Use quote/price-levels API to get prices for these components
        const resp = await fetch('/api/quote/price-levels', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                items: toLoad.map(c => ({ lot_name: c.lot_name, quantity: 1 })),
                pricelist_ids: Object.fromEntries(
                    Object.entries(selectedPricelistIds)
                        .filter(([, id]) => typeof id === 'number' && id > 0)
                )
            })
        });

        if (resp.ok) {
            const data = await resp.json();
            if (data.items) {
                data.items.forEach(item => {
                    // Cache the estimate price (or 0 if not found)
                    componentPricesCache[item.lot_name] = item.estimate_price || 0;
                });
            }
        }
    } catch (e) {
        console.error('Failed to load component prices', e);
    }
}

function hasComponentPrice(lotName) {
    return lotName in componentPricesCache && componentPricesCache[lotName] > 0;
}

// Autocomplete for single select (Base tab)
async function showAutocomplete(category, input) {
    autocompleteInput = input;
    autocompleteCategory = category;
    autocompleteMode = 'single';
    autocompleteIndex = -1;
    const components = getComponentsForCategory(category);
    await ensurePricesLoaded(components);
    filterAutocomplete(category, input.value);
}

function filterAutocomplete(category, search) {
    const components = getComponentsForCategory(category);
    const searchLower = search.toLowerCase();

    autocompleteFiltered = components.filter(c => {
        if (!hasComponentPrice(c.lot_name)) return false;
        if (!isComponentAllowedByStockFilter(c)) return false;
        const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
        return text.includes(searchLower);
    })
    .sort((a, b) => {
        // Sort by popularity_score desc, then by lot_name
        const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0);
        if (popDiff !== 0) return popDiff;
        return a.lot_name.localeCompare(b.lot_name);
    });

    renderAutocomplete();
}

function renderAutocomplete() {
    const dropdown = document.getElementById('autocomplete-dropdown');

    if (autocompleteFiltered.length === 0 || !autocompleteInput) {
        dropdown.classList.add('hidden');
        return;
    }

    const rect = autocompleteInput.getBoundingClientRect();
    dropdown.style.top = (rect.bottom + window.scrollY) + 'px';
    dropdown.style.left = rect.left + 'px';
    dropdown.style.width = Math.max(rect.width, 400) + 'px';

    // Build autocomplete items based on mode
    dropdown.innerHTML = autocompleteFiltered.map((comp, idx) => {
        if (comp.isDivider) {
            return `<div class="px-3 py-1 text-xs text-gray-400 border-t border-gray-200 select-none cursor-default" style="pointer-events:none">── прочие ──</div>`;
        }

        let onmousedown;

        if (autocompleteMode === 'section') {
            onmousedown = `selectAutocompleteItemSection(${idx}, '${autocompleteCategory}')`;
        } else if (autocompleteMode === 'multi') {
            onmousedown = `selectAutocompleteItemMulti(${idx})`;
        } else if (autocompleteMode === 'bom') {
            onmousedown = `selectAutocompleteItemBOM(${idx}, ${autocompleteCategory})`;
        } else if (autocompleteMode === 'edit-item') {
            onmousedown = `selectAutocompleteEditItem(${idx})`;
        } else {
            // single mode
            onmousedown = `selectAutocompleteItem(${idx})`;
        }

        return `
            <div class="autocomplete-item ${idx === autocompleteIndex ? 'selected' : ''}"
                 onmousedown="${onmousedown}">
                <div class="font-mono text-sm">${escapeHtml(comp.lot_name)}</div>
                <div class="text-xs text-gray-500 truncate">${escapeHtml(comp.description || '')}</div>
            </div>
        `;
    }).join('');

    dropdown.classList.remove('hidden');
}

function handleAutocompleteKey(event, category) {
    if (event.key === 'ArrowDown') {
        event.preventDefault();
        autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1);
        renderAutocomplete();
    } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        autocompleteIndex = Math.max(autocompleteIndex - 1, -1);
        renderAutocomplete();
    } else if (event.key === 'Enter') {
        event.preventDefault();
        if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) {
            selectAutocompleteItem(autocompleteIndex);
        }
    } else if (event.key === 'Escape') {
        hideAutocomplete();
    }
}

function selectAutocompleteItem(index) {
    const comp = autocompleteFiltered[index];
    if (!comp || !autocompleteCategory) return;

    // Remove existing item of this category
    cart = cart.filter(item =>
        (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase() !== autocompleteCategory.toUpperCase()
    );

    const qtyInput = document.getElementById('qty-' + autocompleteCategory);
    const qty = parseInt(qtyInput?.value) || 1;
    const price = componentPricesCache[comp.lot_name] || 0;

    cart.push({
        lot_name: comp.lot_name,
        quantity: qty,
        unit_price: price,
        estimate_price: price,
        warehouse_price: null,
        competitor_price: null,
        delta_wh_estimate_abs: null,
        delta_wh_estimate_pct: null,
        delta_comp_estimate_abs: null,
        delta_comp_estimate_pct: null,
        delta_comp_wh_abs: null,
        delta_comp_wh_pct: null,
        price_missing: ['warehouse', 'competitor'],
        description: comp.description || '',
        category: getComponentCategory(comp)
    });

    hideAutocomplete();
    renderTab();
    updateCartUI();
    triggerAutoSave();
    schedulePriceLevelsRefresh({ delay: 80, rerender: true, autosave: false });
}

function hideAutocomplete() {
    document.getElementById('autocomplete-dropdown').classList.add('hidden');
    autocompleteInput = null;
    autocompleteCategory = null;
    autocompleteMode = null;
    autocompleteIndex = -1;
}

// Autocomplete for multi select tabs
async function showAutocompleteMulti(input) {
    autocompleteInput = input;
    autocompleteCategory = null;
    autocompleteMode = 'multi';
    autocompleteIndex = -1;
    const components = getComponentsForTab(currentTab);
    await ensurePricesLoaded(components);
    filterAutocompleteMulti(input.value);
}

function filterAutocompleteMulti(search) {
    const components = getComponentsForTab(currentTab);
    const searchLower = search.toLowerCase();

    // Filter out already added items
    const addedLots = new Set(cart.map(i => i.lot_name));

    autocompleteFiltered = components.filter(c => {
        if (!hasComponentPrice(c.lot_name)) return false;
        if (addedLots.has(c.lot_name)) return false;
        if (!isComponentAllowedByStockFilter(c)) return false;
        const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
        return text.includes(searchLower);
    })
    .sort((a, b) => {
        // Sort by popularity_score desc, then by lot_name
        const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0);
        if (popDiff !== 0) return popDiff;
        return a.lot_name.localeCompare(b.lot_name);
    });

    renderAutocomplete();
}

function handleAutocompleteKeyMulti(event) {
    if (event.key === 'ArrowDown') {
        event.preventDefault();
        autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1);
        renderAutocomplete();
    } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        autocompleteIndex = Math.max(autocompleteIndex - 1, -1);
        renderAutocomplete();
    } else if (event.key === 'Enter') {
        event.preventDefault();
        if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) {
            selectAutocompleteItemMulti(autocompleteIndex);
        }
    } else if (event.key === 'Escape') {
        hideAutocomplete();
    }
}

function selectAutocompleteItemMulti(index) {
    const comp = autocompleteFiltered[index];
    if (!comp) return;

    const qtyInput = document.getElementById('new-qty');
    const qty = parseInt(qtyInput?.value) || 1;
    const price = componentPricesCache[comp.lot_name] || 0;

    cart.push({
        lot_name: comp.lot_name,
        quantity: qty,
        unit_price: price,
        estimate_price: price,
        warehouse_price: null,
        competitor_price: null,
        delta_wh_estimate_abs: null,
        delta_wh_estimate_pct: null,
        delta_comp_estimate_abs: null,
        delta_comp_estimate_pct: null,
        delta_comp_wh_abs: null,
        delta_comp_wh_pct: null,
        price_missing: ['warehouse', 'competitor'],
        description: comp.description || '',
        category: getComponentCategory(comp)
    });

    hideAutocomplete();
    renderTab();
    updateCartUI();
    triggerAutoSave();
    schedulePriceLevelsRefresh({ delay: 80, rerender: true, autosave: false });
}

// Autocomplete for sectioned tabs (like storage with RAID and Disks sections)
async function showAutocompleteSection(sectionId, input) {
    autocompleteInput = input;
    autocompleteCategory = sectionId; // Store section ID
    autocompleteMode = 'section';
    autocompleteIndex = -1;

    // Load prices for tab components
    const components = getComponentsForTab(currentTab);
    await ensurePricesLoaded(components);

    filterAutocompleteSection(sectionId, input.value, input);
}

function filterAutocompleteSection(sectionId, search, inputElement) {
    const searchLower = search.toLowerCase();

    // Get categories from input element's data attribute
    const categoriesStr = inputElement && inputElement.dataset ? inputElement.dataset.categories : '';
    if (!categoriesStr) {
        autocompleteFiltered = [];
        renderAutocomplete();
        return;
    }

    const categoryList = categoriesStr.split(',').map(c => c.trim().toUpperCase());

    // Get components for this section's categories
    const sectionComponents = allComponents.filter(comp => {
        const category = getComponentCategory(comp);
        return categoryList.includes(category);
    });

    // Filter out already added items
    const addedLots = new Set(cart.map(i => i.lot_name));

    autocompleteFiltered = sectionComponents.filter(c => {
        if (!hasComponentPrice(c.lot_name)) return false;
        if (addedLots.has(c.lot_name)) return false;
        if (!isComponentAllowedByStockFilter(c)) return false;
        const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
        return text.includes(searchLower);
    })
    .sort((a, b) => {
        // Sort by popularity_score desc, then by lot_name
        const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0);
        if (popDiff !== 0) return popDiff;
        return a.lot_name.localeCompare(b.lot_name);
    });

    renderAutocomplete();
}

function handleAutocompleteKeySection(event, sectionId) {
    if (event.key === 'ArrowDown') {
        event.preventDefault();
        autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1);
        renderAutocomplete();
    } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        autocompleteIndex = Math.max(autocompleteIndex - 1, -1);
        renderAutocomplete();
    } else if (event.key === 'Enter') {
        event.preventDefault();
        if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) {
            selectAutocompleteItemSection(autocompleteIndex, sectionId);
        }
    } else if (event.key === 'Escape') {
        hideAutocomplete();
    }
}

function selectAutocompleteItemSection(index, sectionId) {
    const comp = autocompleteFiltered[index];
    if (!comp) return;

    const qtyInput = document.getElementById('new-qty-' + sectionId);
    const qty = parseInt(qtyInput?.value) || 1;
    const price = componentPricesCache[comp.lot_name] || 0;

    cart.push({
        lot_name: comp.lot_name,
        quantity: qty,
        unit_price: price,
        estimate_price: price,
        warehouse_price: null,
        competitor_price: null,
        delta_wh_estimate_abs: null,
        delta_wh_estimate_pct: null,
        delta_comp_estimate_abs: null,
        delta_comp_estimate_pct: null,
        delta_comp_wh_abs: null,
        delta_comp_wh_pct: null,
        price_missing: ['warehouse', 'competitor'],
        description: comp.description || '',
        category: getComponentCategory(comp)
    });

    hideAutocomplete();

    // Clear the input field
    const input = document.getElementById('input-section-' + sectionId);
    if (input) input.value = '';

    // Reset quantity to 1
    if (qtyInput) qtyInput.value = '1';
    renderTab();
    updateCartUI();
    triggerAutoSave();
    schedulePriceLevelsRefresh({ delay: 80, rerender: true, autosave: false });
}

// Autocomplete for editing an existing cart item's LOT (multi/section tabs)
async function showAutocompleteEditItem(lotName, input) {
    autocompleteInput = input;
    autocompleteCategory = lotName;
    autocompleteMode = 'edit-item';
    autocompleteIndex = -1;
    autocompleteEditCategories = input.dataset.categories
        ? input.dataset.categories.split(',').map(c => c.trim().toUpperCase())
        : null;
    const components = getComponentsForTab(currentTab);
    await ensurePricesLoaded(components);
    filterAutocompleteEditItem(input.value);
}

function filterAutocompleteEditItem(search) {
    const searchLower = (search || '').toLowerCase();
    const components = autocompleteEditCategories
        ? allComponents.filter(c => autocompleteEditCategories.includes(getComponentCategory(c)))
        : getComponentsForTab(currentTab);
    autocompleteFiltered = components.filter(c => {
        if (!hasComponentPrice(c.lot_name)) return false;
        if (!isComponentAllowedByStockFilter(c)) return false;
        return (c.lot_name + ' ' + (c.description || '')).toLowerCase().includes(searchLower);
    }).sort((a, b) => {
        const d = (b.popularity_score || 0) - (a.popularity_score || 0);
        return d !== 0 ? d : a.lot_name.localeCompare(b.lot_name);
    });
    renderAutocomplete();
}

function handleAutocompleteKeyEditItem(event) {
    if (event.key === 'ArrowDown') {
        event.preventDefault();
        autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1);
        renderAutocomplete();
    } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        autocompleteIndex = Math.max(autocompleteIndex - 1, -1);
        renderAutocomplete();
    } else if (event.key === 'Enter') {
        event.preventDefault();
        if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) {
            selectAutocompleteEditItem(autocompleteIndex);
        }
    } else if (event.key === 'Escape') {
        hideAutocomplete();
    }
}

function selectAutocompleteEditItem(index) {
    const comp = autocompleteFiltered[index];
    if (!comp) return;
    const lotName = autocompleteCategory;
    const oldItem = cart.find(i => i.lot_name === lotName);
    const qty = oldItem?.quantity || 1;
    cart = cart.filter(i => i.lot_name !== lotName);
    const price = componentPricesCache[comp.lot_name] || 0;
    cart.push({
        lot_name: comp.lot_name,
        quantity: qty,
        unit_price: price,
        estimate_price: price,
        warehouse_price: null,
        competitor_price: null,
        delta_wh_estimate_abs: null,
        delta_wh_estimate_pct: null,
        delta_comp_estimate_abs: null,
        delta_comp_estimate_pct: null,
        delta_comp_wh_abs: null,
        delta_comp_wh_pct: null,
        price_missing: ['warehouse', 'competitor'],
        description: comp.description || '',
        category: getComponentCategory(comp)
    });
    hideAutocomplete();
    renderTab();
    updateCartUI();
    triggerAutoSave();
    schedulePriceLevelsRefresh({ delay: 80, rerender: true, autosave: false });
}

// Autocomplete for BOM LOT mapping
function showAutocompleteBOM(rowIdx, input) {
    autocompleteInput = input;
    autocompleteCategory = rowIdx;
    autocompleteMode = 'bom';
    autocompleteIndex = -1;
    filterAutocompleteBOM(rowIdx, input.value);
}

function filterAutocompleteBOM(rowIdx, search) {
    const searchLower = (search || '').toLowerCase();
    const cartLots = new Set(cart.map(i => i.lot_name));
    const all = (window._bomAllComponents || allComponents).filter(c => {
        const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
        return text.includes(searchLower);
    });
    const inCart = all.filter(c => cartLots.has(c.lot_name))
        .sort((a, b) => a.lot_name.localeCompare(b.lot_name));
    const notInCart = all.filter(c => !cartLots.has(c.lot_name))
        .sort((a, b) => {
            const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0);
            if (popDiff !== 0) return popDiff;
            return a.lot_name.localeCompare(b.lot_name);
        });
    if (inCart.length && notInCart.length) {
        autocompleteFiltered = [...inCart, {isDivider: true}, ...notInCart];
    } else {
        autocompleteFiltered = [...inCart, ...notInCart];
    }
    renderAutocomplete();
}

function handleAutocompleteKeyBOM(event, rowIdx) {
    if (event.key === 'ArrowDown') {
        event.preventDefault();
        autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1);
        renderAutocomplete();
    } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        autocompleteIndex = Math.max(autocompleteIndex - 1, -1);
        renderAutocomplete();
    } else if (event.key === 'Enter') {
        event.preventDefault();
        if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) {
            selectAutocompleteItemBOM(autocompleteIndex, rowIdx);
        }
    } else if (event.key === 'Escape') {
        hideAutocomplete();
    }
}

function selectAutocompleteItemBOM(index, rowIdx) {
    const comp = autocompleteFiltered[index];
    if (!comp || comp.isDivider) return;
    const row = bomRows.find(r => r.source_row_index === rowIdx) || bomRows[rowIdx];
    if (!row) return;
    row.manual_lot = comp.lot_name;
    hideAutocomplete();
    resolveBOM();
}

function clearSingleSelect(category) {
    cart = cart.filter(item =>
        (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase() !== category.toUpperCase()
    );
    renderTab();
    updateCartUI();
    triggerAutoSave();
}

function updateSingleQuantity(category, value) {
    const qty = parseInt(value) || 1;
    const item = cart.find(i =>
        (i.category || getCategoryFromLotName(i.lot_name)).toUpperCase() === category.toUpperCase()
    );

    if (item) {
        item.quantity = Math.max(1, qty);
        renderTab();
        updateCartUI();
        triggerAutoSave();
    }
}

function updateMultiQuantity(lotName, value) {
    const qty = parseInt(value) || 1;
    const item = cart.find(i => i.lot_name === lotName);

    if (item) {
        item.quantity = Math.max(1, qty);
        updateCartUI();
        triggerAutoSave();
        // Update total in the row
        const row = document.querySelector(`input[onchange*="${lotName}"]`)?.closest('tr');
        if (row) {
            const totalCell = row.querySelector('td:nth-child(5)');
            if (totalCell) {
                totalCell.textContent = formatMoney(getDisplayPrice(item) * item.quantity);
            }
        }
    }
}

function removeFromCart(lotName) {
    cart = cart.filter(i => i.lot_name !== lotName);
    updateTabVisibility();
    renderTab();
    updateCartUI();
    triggerAutoSave();
}

function updateCartUI() {
    updateTabVisibility();
    updateRequiredCategoryBadges();
    window._currentCart = cart; // expose for BOM/Pricing tabs
    const total = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);
    document.getElementById('cart-total').textContent = formatMoney(total);

    // Recalculate custom price section if active
    calculateCustomPrice();
    renderSalePriceTable();

    scheduleArticlePreview();

    if (cart.length === 0) {
        document.getElementById('cart-items').innerHTML =
            '<div class="text-gray-500 text-center py-2">Конфигурация пуста</div>';
        return;
    }

    // Sort cart items by category display order
    const sortedCart = [...cart].sort((a, b) => {
        const catA = (a.category || getCategoryFromLotName(a.lot_name)).toUpperCase();
        const catB = (b.category || getCategoryFromLotName(b.lot_name)).toUpperCase();
        const orderA = categoryOrderMap[catA] || 9999;
        const orderB = categoryOrderMap[catB] || 9999;
        return orderA - orderB;
    });

    const grouped = {};
    sortedCart.forEach(item => {
        const cat = item.category || getCategoryFromLotName(item.lot_name);
        const tab = getTabForCategory(cat);
        if (!grouped[tab]) grouped[tab] = [];
        grouped[tab].push(item);
    });

    // Sort tabs by minimum display order of their categories
    const sortedTabs = Object.entries(grouped).sort((a, b) => {
        const minOrderA = Math.min(...a[1].map(item => {
            const cat = (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase();
            return categoryOrderMap[cat] || 9999;
        }));
        const minOrderB = Math.min(...b[1].map(item => {
            const cat = (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase();
            return categoryOrderMap[cat] || 9999;
        }));
        return minOrderA - minOrderB;
    });

    let html = '';
    for (const [tab, items] of sortedTabs) {
        const tabLabel = TAB_CONFIG[tab]?.label || tab;
        html += `<div class="mb-2"><div class="text-xs font-medium text-gray-400 uppercase mb-1">${tabLabel}</div>`;

        items.forEach(item => {
            const itemTotal = getDisplayPrice(item) * item.quantity;
            html += `
                <div class="flex justify-between items-center py-1 text-sm">
                    <div class="flex-1">
                        <span class="font-mono">${escapeHtml(item.lot_name)}</span>
                        <span class="text-gray-400 mx-1">×</span>
                        <span>${item.quantity}</span>
                    </div>
                    <div class="flex items-center space-x-2">
                        <span>${formatMoney(itemTotal)}</span>
                        <button onclick="removeFromCart('${item.lot_name}')" class="text-red-500 hover:text-red-700">
                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                            </svg>
                        </button>
                    </div>
                </div>
            `;
        });

        html += '</div>';
    }

    document.getElementById('cart-items').innerHTML = html;
}

function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

function updateServerModelForQuote(value) {
    serverModelForQuote = value || '';
    scheduleArticlePreview();
}

function updateSupportCode(value) {
    supportCode = value || '';
    scheduleArticlePreview();
}

function scheduleArticlePreview() {
    if (articlePreviewTimeout) {
        clearTimeout(articlePreviewTimeout);
    }
    articlePreviewTimeout = setTimeout(() => {
        previewArticle();
    }, 250);
}

async function previewArticle() {
    const el = document.getElementById('article-display');
    if (!el) return;

    const model = serverModelForQuote.trim();
    const estimatePricelistID = getEffectivePricelistID('estimate');
    if (!model || !estimatePricelistID || cart.length === 0) {
        currentArticle = '';
        el.textContent = 'Артикул: —';
        return;
    }

    try {
        const resp = await fetch('/api/configs/preview-article', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                server_model: serverModelForQuote,
                support_code: supportCode,
                pricelist_id: estimatePricelistID,
                items: cart.map(item => ({
                    lot_name: item.lot_name,
                    quantity: item.quantity,
                    unit_price: item.unit_price || 0
                }))
            })
        });
        if (!resp.ok) {
            currentArticle = '';
            el.textContent = 'Артикул: —';
            return;
        }
        const data = await resp.json();
        currentArticle = data.article || '';
        el.textContent = currentArticle ? ('Артикул: ' + currentArticle) : 'Артикул: —';
    } catch(e) {
        currentArticle = '';
        el.textContent = 'Артикул: —';
    }
}

function getCurrentArticle() {
    return currentArticle || '';
}

function buildPricingState() {
    const buyCustom = parseDecimalInput(document.getElementById('pricing-custom-price-buy')?.value || '');
    const saleUplift = parseDecimalInput(document.getElementById('pricing-uplift-sale')?.value || '');
    const saleCustom = parseDecimalInput(document.getElementById('pricing-custom-price-sale')?.value || '');

    return {
        buy_custom_price: buyCustom > 0 ? buyCustom : null,
        sale_uplift: saleUplift > 0 ? saleUplift : null,
        sale_custom_price: saleCustom > 0 ? saleCustom : null,
    };
}

function serializeConfigNotes() {
    return JSON.stringify({
        pricing_ui: buildPricingState()
    });
}

function restorePricingStateFromNotes(notesRaw) {
    if (!notesRaw) return;
    let parsed;
    try {
        parsed = JSON.parse(notesRaw);
    } catch (_) {
        return;
    }

    const pricing = parsed?.pricing_ui;
    if (!pricing || typeof pricing !== 'object') return;

    const buyInput = document.getElementById('pricing-custom-price-buy');
    if (buyInput) {
        buyInput.value = typeof pricing.buy_custom_price === 'number' && pricing.buy_custom_price > 0
            ? pricing.buy_custom_price.toFixed(2)
            : '';
    }

    const upliftInput = document.getElementById('pricing-uplift-sale');
    if (upliftInput) {
        upliftInput.value = typeof pricing.sale_uplift === 'number' && pricing.sale_uplift > 0
            ? formatUpliftInput(pricing.sale_uplift)
            : '';
    }

    const saleInput = document.getElementById('pricing-custom-price-sale');
    if (saleInput) {
        saleInput.value = typeof pricing.sale_custom_price === 'number' && pricing.sale_custom_price > 0
            ? pricing.sale_custom_price.toFixed(2)
            : '';
    }
}

function getAutosaveStorageKey() {
    return `qf_config_autosave_${configUUID || 'default'}`;
}

function buildSavePayload() {
    const customPriceInput = document.getElementById('custom-price-input');
    const customPriceValue = parseFloat(customPriceInput.value);
    const customPrice = customPriceValue > 0 ? customPriceValue : null;

    return {
        name: configName,
        items: cart,
        custom_price: customPrice,
        notes: serializeConfigNotes(),
        server_count: serverCount,
        server_model: serverModelForQuote,
        support_code: supportCode,
        article: getCurrentArticle(),
        pricelist_id: selectedPricelistIds.estimate,
        warehouse_pricelist_id: selectedPricelistIds.warehouse,
        competitor_pricelist_id: selectedPricelistIds.competitor,
        disable_price_refresh: disablePriceRefresh,
        only_in_stock: onlyInStock
    };
}

function persistAutosaveDraft() {
    if (!configUUID) return;
    try {
        sessionStorage.setItem(getAutosaveStorageKey(), JSON.stringify({
            payload: buildSavePayload(),
            saved_at: Date.now()
        }));
    } catch (_) {
        // ignore storage failures
    }
}

function clearAutosaveDraft() {
    try {
        sessionStorage.removeItem(getAutosaveStorageKey());
    } catch (_) {
        // ignore storage failures
    }
}

function restoreAutosaveDraftIfAny() {
    if (!configUUID) return;
    let raw = null;
    try {
        raw = sessionStorage.getItem(getAutosaveStorageKey());
    } catch (_) {
        raw = null;
    }
    if (!raw) return;

    try {
        const parsed = JSON.parse(raw);
        const payload = parsed && parsed.payload ? parsed.payload : null;
        if (!payload) return;

        if (Array.isArray(payload.items)) {
            cart = payload.items.map(item => ({
                lot_name: item.lot_name,
                quantity: item.quantity,
                unit_price: item.unit_price,
                estimate_price: item.unit_price,
                warehouse_price: null,
                competitor_price: null,
                description: item.description || '',
                category: item.category || getCategoryFromLotName(item.lot_name)
            }));
        }
        if (typeof payload.server_count === 'number' && payload.server_count > 0) {
            serverCount = payload.server_count;
            const serverCountInput = document.getElementById('server-count');
            if (serverCountInput) serverCountInput.value = serverCount;
            const totalServerCount = document.getElementById('total-server-count');
            if (totalServerCount) totalServerCount.textContent = serverCount;
        }
        serverModelForQuote = payload.server_model || serverModelForQuote;
        supportCode = payload.support_code || supportCode;
        currentArticle = payload.article || currentArticle;
        selectedPricelistIds.estimate = payload.pricelist_id || selectedPricelistIds.estimate;
        onlyInStock = Boolean(payload.only_in_stock);

        const customPriceInput = document.getElementById('custom-price-input');
        if (customPriceInput) {
            if (typeof payload.custom_price === 'number' && payload.custom_price > 0) {
                customPriceInput.value = payload.custom_price.toFixed(2);
            } else {
                customPriceInput.value = '';
            }
        }
        if (payload.notes) {
            restorePricingStateFromNotes(payload.notes);
        }
        hasUnsavedChanges = true;
    } catch (_) {
        // ignore invalid draft
    }
}

function saveConfigOnExit() {
    if (!configUUID || !hasUnsavedChanges || exitSaveStarted) return;
    exitSaveStarted = true;
    try {
        fetch('/api/configs/' + configUUID, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(buildSavePayload()),
            keepalive: true
        });
    } catch (_) {
        // best effort save on page exit
    }
}

function triggerAutoSave() {
    // Autosave keeps local draft only; server revision is created on Save/Exit.
    hasUnsavedChanges = true;
    if (autoSaveTimeout) {
        clearTimeout(autoSaveTimeout);
    }
    autoSaveTimeout = setTimeout(() => {
        persistAutosaveDraft();
    }, 1000);
}

async function saveConfig(showNotification = true) {
    // RBAC disabled - no token check required
    if (!configUUID) return;
    if (priceLevelsRefreshTimer) {
        clearTimeout(priceLevelsRefreshTimer);
        priceLevelsRefreshTimer = null;
    }

    await refreshPriceLevels({ force: true, noCache: true });

    try {
        const resp = await fetch('/api/configs/' + configUUID, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(buildSavePayload())
        });

        if (!resp.ok) {
            if (showNotification) {
                showToast('Ошибка сохранения', 'error');
            }
            return;
        }

        const saved = await resp.json();
        if (saved && saved.current_version_no) {
            currentVersionNo = saved.current_version_no;
        }
        updateConfigBreadcrumbs();
        hasUnsavedChanges = false;
        clearAutosaveDraft();
        exitSaveStarted = false;

        if (showNotification) {
            showToast('Сохранено', 'success');
        }
    } catch(e) {
        if (showNotification) {
            showToast('Ошибка сохранения', 'error');
        }
    }
}

// Helper function to extract filename from Content-Disposition header
function getFilenameFromResponse(resp) {
    const contentDisposition = resp.headers.get('content-disposition');
    if (!contentDisposition) return null;
    const matches = contentDisposition.match(/filename="?([^"]+)"?/);
    return matches && matches[1] ? matches[1] : null;
}

async function exportCSV() {
    if (cart.length === 0) return;

    try {
        if (priceLevelsRefreshTimer) {
            clearTimeout(priceLevelsRefreshTimer);
            priceLevelsRefreshTimer = null;
        }
        await refreshPriceLevels({ force: true, noCache: true });

        const exportItems = cart.map(item => ({
            ...item,
            unit_price: getDisplayPrice(item),
        }));
        const article = getCurrentArticle();
        const resp = await fetch('/api/export/csv', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({items: exportItems, name: configName, project_uuid: projectUUID, article: article, server_count: serverCount, pricelist_id: selectedPricelistIds.estimate || null})
        });

        const blob = await resp.blob();
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        const articleForName = article || 'BOM';
        a.download = getFilenameFromResponse(resp) || ((configName || 'config') + ' ' + articleForName + '.csv');
        a.click();
        window.URL.revokeObjectURL(url);
    } catch(e) {
        showToast('Ошибка экспорта', 'error');
    }
}

function formatLineTotalTooltip(qty, unitPrice) {
    if (typeof unitPrice !== 'number' || unitPrice <= 0) return '';
    const lineTotal = qty * unitPrice;
    return `${formatNumberRu(qty)} * ${formatMoney(unitPrice)} = ${formatMoney(lineTotal)}`;
}

function toggleSection(contentId, iconId) {
    const content = document.getElementById(contentId);
    const icon = document.getElementById(iconId);
    if (!content || !icon) return;

    const isHidden = content.classList.toggle('hidden');
    if (isHidden) {
        icon.classList.add('-rotate-90');
    } else {
        icon.classList.remove('-rotate-90');
    }
}

function toggleCartSummarySection() {
    toggleSection('cart-summary-content', 'cart-summary-toggle-icon');
}

function toggleCustomPriceSection() {
    toggleSection('custom-price-content', 'custom-price-toggle-icon');
}

function toggleSalePriceSection() {
    toggleSection('sale-price-content', 'sale-price-toggle-icon');
}

function formatDiffPercent(baseTotal, compareTotal, compareLabel) {
    if (typeof baseTotal !== 'number' || typeof compareTotal !== 'number' || compareTotal <= 0) {
        return `N/A от ${compareLabel}`;
    }
    const pct = ((baseTotal - compareTotal) / compareTotal) * 100;
    const sign = pct > 0 ? '+' : '';
    return `${sign}${pct.toFixed(1).replace('.', ',')}% от ${compareLabel}`;
}

function getTotalClass(current, references) {
    const validRefs = references.filter(v => typeof v === 'number' && v > 0);
    if (typeof current !== 'number' || current <= 0 || validRefs.length === 0) {
        return 'text-gray-900';
    }
    const avg = validRefs.reduce((sum, v) => sum + v, 0) / validRefs.length;
    if (current < avg) return 'text-green-600';
    if (current > avg) return 'text-red-600';
    return 'text-gray-900';
}

function renderSalePriceTable() {
    const body = document.getElementById('sale-prices-body');
    const totalEstEl = document.getElementById('sale-total-est');
    const totalWarehouseEl = document.getElementById('sale-total-warehouse');
    const totalCompetitorEl = document.getElementById('sale-total-competitor');
    if (!body || !totalEstEl || !totalWarehouseEl || !totalCompetitorEl) return;

    if (cart.length === 0) {
        body.innerHTML = '<tr><td colspan="5" class="px-3 py-3 text-center text-gray-500">Конфигурация пуста</td></tr>';
        totalEstEl.textContent = '$0.00';
        totalWarehouseEl.textContent = '$0.00';
        totalCompetitorEl.textContent = '$0.00';
        totalEstEl.title = '';
        totalWarehouseEl.title = '';
        totalCompetitorEl.title = '';
        totalEstEl.className = 'px-3 py-2 text-right';
        totalWarehouseEl.className = 'px-3 py-2 text-right';
        totalCompetitorEl.className = 'px-3 py-2 text-right';
        return;
    }

    const sortedCart = [...cart].sort((a, b) => {
        const catA = (a.category || getCategoryFromLotName(a.lot_name)).toUpperCase();
        const catB = (b.category || getCategoryFromLotName(b.lot_name)).toUpperCase();
        const orderA = categoryOrderMap[catA] || 9999;
        const orderB = categoryOrderMap[catB] || 9999;
        return orderA - orderB;
    });

    let html = '';
    let totalEstimate = 0;
    let totalWarehouse = 0;
    let totalCompetitor = 0;

    sortedCart.forEach(item => {
        const qty = item.quantity || 1;
        const estPrice = item.estimate_price;
        const warehousePrice = item.warehouse_price;
        const competitorPrice = item.competitor_price;

        if (typeof estPrice === 'number' && estPrice > 0) totalEstimate += estPrice * qty;
        if (typeof warehousePrice === 'number' && warehousePrice > 0) totalWarehouse += warehousePrice * qty;
        if (typeof competitorPrice === 'number' && competitorPrice > 0) totalCompetitor += competitorPrice * qty;

        const estTooltip = formatLineTotalTooltip(qty, estPrice);
        const warehouseTooltip = formatLineTotalTooltip(qty, warehousePrice);
        const competitorTooltip = formatLineTotalTooltip(qty, competitorPrice);

        html += `
            <tr>
                <td class="px-3 py-2 font-mono">${escapeHtml(item.lot_name)}</td>
                <td class="px-3 py-2 text-right">${qty}</td>
                <td class="px-3 py-2 text-right" title="${escapeHtml(estTooltip)}">${formatPriceOrNA(estPrice)}</td>
                <td class="px-3 py-2 text-right" title="${escapeHtml(warehouseTooltip)}">${formatPriceOrNA(warehousePrice)}</td>
                <td class="px-3 py-2 text-right" title="${escapeHtml(competitorTooltip)}">${formatPriceOrNA(competitorPrice)}</td>
            </tr>
        `;
    });

    body.innerHTML = html;

    totalEstEl.textContent = formatMoney(totalEstimate);
    totalWarehouseEl.textContent = formatMoney(totalWarehouse);
    totalCompetitorEl.textContent = formatMoney(totalCompetitor);

    totalEstEl.title =
        `${formatDiffPercent(totalEstimate, totalWarehouse, 'склад')}\n` +
        `${formatDiffPercent(totalEstimate, totalCompetitor, 'конкуренты')}`;
    totalWarehouseEl.title =
        `${formatDiffPercent(totalWarehouse, totalEstimate, 'est.price')}\n` +
        `${formatDiffPercent(totalWarehouse, totalCompetitor, 'конкуренты')}`;
    totalCompetitorEl.title =
        `${formatDiffPercent(totalCompetitor, totalEstimate, 'est.price')}\n` +
        `${formatDiffPercent(totalCompetitor, totalWarehouse, 'склад')}`;

    totalEstEl.className = `px-3 py-2 text-right ${getTotalClass(totalEstimate, [totalWarehouse, totalCompetitor])}`;
    totalWarehouseEl.className = `px-3 py-2 text-right ${getTotalClass(totalWarehouse, [totalEstimate, totalCompetitor])}`;
    totalCompetitorEl.className = `px-3 py-2 text-right ${getTotalClass(totalCompetitor, [totalEstimate, totalWarehouse])}`;
}

// Custom price functionality
function calculateCustomPrice() {
    const customPriceInput = document.getElementById('custom-price-input');
    const adjustedPricesEl = document.getElementById('adjusted-prices');
    const discountInfoEl = document.getElementById('discount-info');
    const discountPercentEl = document.getElementById('discount-percent');
    const adjustedPricesBodyEl = document.getElementById('adjusted-prices-body');
    const adjustedTotalOriginalEl = document.getElementById('adjusted-total-original');
    const adjustedTotalNewEl = document.getElementById('adjusted-total-new');
    const adjustedTotalFinalEl = document.getElementById('adjusted-total-final');

    if (!customPriceInput || !adjustedPricesEl || !discountInfoEl || !discountPercentEl ||
        !adjustedPricesBodyEl || !adjustedTotalOriginalEl || !adjustedTotalNewEl || !adjustedTotalFinalEl) {
        return;
    }
    const customPrice = parseFloat(customPriceInput.value) || 0;

    const originalTotal = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);

    if (customPrice <= 0 || cart.length === 0 || originalTotal <= 0) {
        adjustedPricesEl.classList.add('hidden');
        discountInfoEl.classList.add('hidden');
        return;
    }

    // Calculate discount percentage
    const discountPercent = ((originalTotal - customPrice) / originalTotal) * 100;
    const coefficient = customPrice / originalTotal;

    // Show discount info
    discountInfoEl.classList.remove('hidden');
    discountPercentEl.textContent = discountPercent.toFixed(1).replace('.', ',') + '%';

    // Update discount color based on value
    const discountEl = discountPercentEl;
    if (discountPercent > 0) {
        discountEl.className = 'text-2xl font-bold text-green-600';
    } else if (discountPercent < 0) {
        discountEl.className = 'text-2xl font-bold text-red-600';
    } else {
        discountEl.className = 'text-2xl font-bold text-gray-600';
    }

    // Build adjusted prices table
    // Sort cart items by category display order
    const sortedCart = [...cart].sort((a, b) => {
        const catA = (a.category || getCategoryFromLotName(a.lot_name)).toUpperCase();
        const catB = (b.category || getCategoryFromLotName(b.lot_name)).toUpperCase();
        const orderA = categoryOrderMap[catA] || 9999;
        const orderB = categoryOrderMap[catB] || 9999;
        return orderA - orderB;
    });

    let html = '';
    let totalOriginal = 0;
    let totalNew = 0;

    sortedCart.forEach(item => {
        const originalPrice = getDisplayPrice(item);
        const newPrice = originalPrice * coefficient;
        const itemOriginalTotal = originalPrice * item.quantity;
        const itemNewTotal = newPrice * item.quantity;

        totalOriginal += itemOriginalTotal;
        totalNew += itemNewTotal;

        html += `
            <tr>
                <td class="px-3 py-2 font-mono">${escapeHtml(item.lot_name)}</td>
                <td class="px-3 py-2 text-right">${item.quantity}</td>
                <td class="px-3 py-2 text-right text-gray-500">${formatMoney(originalPrice)}</td>
                <td class="px-3 py-2 text-right text-green-600">${formatMoney(newPrice)}</td>
                <td class="px-3 py-2 text-right">${formatMoney(itemNewTotal)}</td>
            </tr>
        `;
    });

    adjustedPricesBodyEl.innerHTML = html;
    adjustedTotalOriginalEl.textContent = formatMoney(totalOriginal);
    adjustedTotalNewEl.textContent = formatMoney(totalNew);
    adjustedTotalFinalEl.textContent = formatMoney(totalNew);
    adjustedPricesEl.classList.remove('hidden');
}

function clearCustomPrice() {
    const customPriceInput = document.getElementById('custom-price-input');
    const adjustedPricesEl = document.getElementById('adjusted-prices');
    const discountInfoEl = document.getElementById('discount-info');

    if (customPriceInput) customPriceInput.value = '';
    if (adjustedPricesEl) adjustedPricesEl.classList.add('hidden');
    if (discountInfoEl) discountInfoEl.classList.add('hidden');
    triggerAutoSave();
}

async function exportCSVWithCustomPrice() {
    if (cart.length === 0) return;
    if (priceLevelsRefreshTimer) {
        clearTimeout(priceLevelsRefreshTimer);
        priceLevelsRefreshTimer = null;
    }
    await refreshPriceLevels({ force: true, noCache: true });

    const customPrice = parseFloat(document.getElementById('custom-price-input').value) || 0;
    const originalTotal = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);

    if (customPrice <= 0 || originalTotal <= 0) {
        showToast('Введите целевую цену', 'error');
        return;
    }

    const coefficient = customPrice / originalTotal;

    // Create adjusted cart
    const adjustedCart = cart.map(item => ({
        ...item,
        unit_price: parseFloat((getDisplayPrice(item) * coefficient).toFixed(2))
    }));

    try {
        const resp = await fetch('/api/export/csv', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({items: adjustedCart, name: configName, project_uuid: projectUUID})
        });

        const blob = await resp.blob();
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = getFilenameFromResponse(resp) || (configName || 'config') + '.csv';
        a.click();
        window.URL.revokeObjectURL(url);
    } catch(e) {
        showToast('Ошибка экспорта', 'error');
    }
}

async function refreshPrices() {
    if (!configUUID) return;
    if (disablePriceRefresh) {
        showToast('Обновление цен отключено в настройках', 'error');
        return;
    }

    const refreshBtn = document.getElementById('refresh-prices-btn');
    const previousLabel = refreshBtn ? refreshBtn.textContent : '';

    try {
        if (refreshBtn) {
            refreshBtn.disabled = true;
            refreshBtn.textContent = 'Обновление...';
            refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
        }

        await loadActivePricelists(true);

        ['estimate', 'warehouse', 'competitor'].forEach(source => {
            const latest = activePricelistsBySource[source]?.[0];
            if (latest && latest.id) {
                selectedPricelistIds[source] = Number(latest.id);
                resolvedAutoPricelistIds[source] = null;
            }
        });

        syncPriceSettingsControls();
        renderPricelistSettingsSummary();
        persistLocalPriceSettings();

        // Snapshot prices before refresh for diff
        const beforePricesMap = {};
        let beforeTotal = 0;
        for (const item of cart) {
            const p = getDisplayPrice(item);
            beforePricesMap[item.lot_name] = { price: p, qty: item.quantity };
            beforeTotal += p * item.quantity;
        }
        beforeTotal *= serverCount;

        // Create a revision of the current state before prices are updated
        if (configUUID) {
            try {
                await fetch('/api/configs/' + configUUID + '/snapshot', { method: 'POST' });
            } catch (e) {
                console.warn('pre-refresh snapshot failed', e);
            }
        }

        await saveConfig(false);
        await refreshPriceLevels({ force: true, noCache: true });
        renderTab();
        updateCartUI();

        // Compute diff after refresh
        const itemDiffs = [];
        let afterTotal = 0;
        for (const item of cart) {
            const newPrice = getDisplayPrice(item);
            afterTotal += newPrice * item.quantity;
            const before = beforePricesMap[item.lot_name];
            if (before && Math.abs(before.price - newPrice) > 0.01) {
                itemDiffs.push({ lot_name: item.lot_name, quantity: item.quantity, prevPrice: before.price, newPrice });
            }
        }
        afterTotal *= serverCount;

        if (configUUID) {
            const configResp = await fetch('/api/configs/' + configUUID);
            if (configResp.ok) {
                const config = await configResp.json();
                if (config.price_updated_at) updatePriceUpdateDate(config.price_updated_at);
            }
        }

        showToast('Цены обновлены', 'success');
        showPriceDiffModal([{ configName: configName || 'Конфигурация', prevTotal: beforeTotal, newTotal: afterTotal, serverCount, itemDiffs }]);
    } catch(e) {
        showToast('Ошибка обновления цен', 'error');
    } finally {
        if (refreshBtn) {
            refreshBtn.disabled = false;
            refreshBtn.textContent = previousLabel || 'Обновить цены';
            updateRefreshPricesButtonState();
        }
    }
}

function updatePriceUpdateDate(dateStr) {
    if (!dateStr) {
        document.getElementById('price-update-date').textContent = '';
        return;
    }

    const date = new Date(dateStr);
    const now = new Date();
    const diffMs = now - date;
    const diffMins = Math.floor(diffMs / 60000);
    const diffHours = Math.floor(diffMs / 3600000);
    const diffDays = Math.floor(diffMs / 86400000);

    let timeAgo;
    if (diffMins < 1) {
        timeAgo = 'только что';
    } else if (diffMins < 60) {
        timeAgo = diffMins + ' мин. назад';
    } else if (diffHours < 24) {
        timeAgo = diffHours + ' ч. назад';
    } else if (diffDays < 7) {
        timeAgo = diffDays + ' дн. назад';
    } else {
        timeAgo = date.toLocaleDateString('ru-RU');
    }

    document.getElementById('price-update-date').textContent = 'Обновлено: ' + timeAgo;
}

// ==================== TOP-LEVEL TABS ====================

let currentTopTab = 'estimate';

function switchTopTab(tab) {
    currentTopTab = tab;
    const tabs = ['estimate', 'bom', 'pricing'];
    tabs.forEach(t => {
        const btn = document.getElementById('top-tab-' + t);
        const section = document.getElementById('top-section-' + t);
        if (t === tab) {
            btn.classList.remove('border-transparent', 'text-gray-500');
            btn.classList.add('border-blue-600', 'text-blue-600');
            section.classList.remove('hidden');
        } else {
            btn.classList.remove('border-blue-600', 'text-blue-600');
            btn.classList.add('border-transparent', 'text-gray-500');
            section.classList.add('hidden');
        }
    });
    if (tab === 'pricing') {
        renderPricingTab();
    }
}

// ==================== BOM ВЕНДОРА ====================

let bomRows = []; // [{vendor_pn, quantity, description, unit_price, total_price, resolved_lot, resolution_source}]
let bomImportRaw = null; // { mode:'raw'|'parsed', rows, columnTypes, ignoredRows, rowErrors, uiError }

const BOM_COL_TYPES = [
    { value: 'ignore', label: 'Не использовать' },
    { value: 'pn', label: 'P/N' },
    { value: 'qty', label: 'Кол-во' },
    { value: 'price', label: 'Цена' },
    { value: 'description', label: 'Описание' }
];

function _bomRawHeaderWidthClass(colType) {
    switch (colType) {
        case 'qty': return 'w-24 min-w-24';
        case 'price': return 'w-32 min-w-32';
        case 'pn': return 'min-w-40';
        case 'description': return 'min-w-48';
        default: return 'min-w-28';
    }
}

function _bomRawCellWidthClass(colType) {
    switch (colType) {
        case 'qty': return 'w-24 min-w-24';
        case 'price': return 'w-32 min-w-32';
        default: return '';
    }
}

function parsePastePrice(s) {
    if (!s) return null;
    let v = String(s).replace(/[$\s]/g, '');
    if (/,\d{1,2}$/.test(v)) v = v.replace(/\./g, '').replace(',', '.');
    else v = v.replace(/,/g, '');
    const n = parseFloat(v);
    return isNaN(n) ? null : n;
}

function _ensureBomDatalist() {
    let dl = document.getElementById('lot-autocomplete-list');
    if (!dl) {
        dl = document.createElement('datalist');
        dl.id = 'lot-autocomplete-list';
        document.body.appendChild(dl);
    }
    const all = _bomLots();
    const cartLots = [];
    const seenCart = new Set();
    (window._currentCart || []).forEach(item => {
        const lot = (item?.lot_name || '').trim();
        if (!lot || seenCart.has(lot)) return;
        seenCart.add(lot);
        cartLots.push(lot);
    });

    const mappedSet = new Set();
    bomRows.forEach(r => {
        _getRowCanonicalLotMappings(r).forEach(m => {
            if (m?.lot_name) mappedSet.add(m.lot_name);
        });
    });

    const priorityLots = cartLots.filter(l => !mappedSet.has(l));
    const prioritySet = new Set(priorityLots);
    const rest = all.filter(l => !prioritySet.has(l));
    const parts = [];
    priorityLots.forEach(l => parts.push(`<option value="${escapeHtml(l)}">`));
    if (priorityLots.length && rest.length) {
        // Visual separator inside datalist suggestions.
        parts.push(`<option value="${BOM_LOT_DATALIST_DIVIDER}">`);
    }
    rest.forEach(l => parts.push(`<option value="${escapeHtml(l)}">`));
    dl.innerHTML = parts.join('');
    return dl;
}

function _setBomUIError(msg) {
    const el = document.getElementById('bom-errors');
    if (!el) return;
    if (msg) {
        el.textContent = msg;
        el.classList.remove('hidden');
    } else {
        el.textContent = '';
        el.classList.add('hidden');
    }
}

function _bomRawColCount() {
    if (!bomImportRaw || !Array.isArray(bomImportRaw.rows)) return 0;
    return bomImportRaw.rows.reduce((m, r) => Math.max(m, Array.isArray(r) ? r.length : 0), 0);
}

function _normalizeBomRawRows(rows) {
    const ncols = rows.reduce((m, r) => Math.max(m, r.length), 0);
    return rows.map(r => {
        const row = r.slice();
        while (row.length < ncols) row.push('');
        return row;
    });
}

function _applyParsedBOMRows(parsed) {
    if (!Array.isArray(parsed) || !parsed.length) return false;
    bomImportRaw = {
        mode: 'raw',
        rows: parsed,
        columnTypes: ['pn', 'qty'],
        ignoredRows: {},
        rowErrors: {},
        uiError: ''
    };
    bomRows = [];
    _setBomUIError('');
    rebuildBOMRowsFromRaw();
    renderBOMTable();
    return true;
}

// Detection and parsing of known single-column text BOM formats (Inspur, Russian
// text BOM) lives only on the server (internal/services/vendor_workspace_import.go),
// shared with the vendor file-import path. The paste handler asks the server to
// parse; an unrecognized payload falls back to the generic Excel column grid below.
async function _serverParseBOMText(text) {
    try {
        const resp = await fetch('/api/vendor-spec/parse-text', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({text})
        });
        if (!resp.ok) return null;
        const data = await resp.json();
        if (!Array.isArray(data.rows) || !data.rows.length) return null;
        return data.rows.map(r => [r.vendor_partnumber || '', String(r.quantity || '')]);
    } catch (e) {
        return null;
    }
}

function _applyGenericBOMPaste(text) {
    const lines = text.split(/\r?\n/).filter(l => l.length > 0);
    if (!lines.length) return;
    const rows = lines.map(l => l.split('\t').map(c => c.trim()));
    const normalized = _normalizeBomRawRows(rows);
    if (!normalized.length || !(normalized[0] && normalized[0].length)) return;

    bomImportRaw = {
        mode: 'raw',
        rows: normalized,
        columnTypes: Array(normalized[0]?.length || 0).fill('ignore'),
        ignoredRows: {},
        rowErrors: {},
        uiError: ''
    };
    bomRows = [];
    _setBomUIError('');
    renderBOMTable();
}

async function handleBOMPaste(event) {
    event.preventDefault();
    const text = event.clipboardData.getData('text/plain');
    if (!text || !text.trim()) return;

    // Tabs mean a real spreadsheet table — go straight to the column grid.
    if (!text.includes('\t')) {
        const parsed = await _serverParseBOMText(text);
        if (_applyParsedBOMRows(parsed)) return;
    }

    _applyGenericBOMPaste(text);
}

function _getBomColumnTypeIndexes() {
    if (!bomImportRaw || !Array.isArray(bomImportRaw.columnTypes)) return null;
    const idx = { ignore: [], pn: [], qty: [], price: [], description: [] };
    bomImportRaw.columnTypes.forEach((t, i) => { (idx[t] || idx.ignore).push(i); });
    return idx;
}

function _validateBomColumnTypes() {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw') return { ok: true };
    const idx = _getBomColumnTypeIndexes();
    if (!idx) return { ok: false, message: 'Нет данных BOM' };
    if (idx.pn.length !== 1) return { ok: false, message: 'Выберите ровно один столбец P/N.' };
    if (idx.qty.length !== 1) return { ok: false, message: 'Выберите ровно один столбец Кол-во.' };
    if (idx.price.length > 1) return { ok: false, message: 'Можно выбрать только один столбец Цена.' };
    if (idx.description.length > 1) return { ok: false, message: 'Можно выбрать только один столбец Описание.' };
    return { ok: true, idx };
}

function onBOMColumnTypeChange(colIdx, value) {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw') return;
    bomImportRaw.columnTypes[colIdx] = value;
    rebuildBOMRowsFromRaw();
}

function onBOMRawCellInput(rowIdx, colIdx, value) {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw' || !bomImportRaw.rows[rowIdx]) return;
    bomImportRaw.rows[rowIdx][colIdx] = value;
    debouncedRebuildBOMFromRaw();
}

function toggleBOMRawRowIgnored(rowIdx) {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw') return;
    bomImportRaw.ignoredRows[rowIdx] = !bomImportRaw.ignoredRows[rowIdx];
    rebuildBOMRowsFromRaw();
}

function deleteBOMRawRow(rowIdx) {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw') return;
    bomImportRaw.rows.splice(rowIdx, 1);
    const nextIgnored = {};
    const nextErrors = {};
    Object.entries(bomImportRaw.ignoredRows || {}).forEach(([k, v]) => {
        const n = Number(k);
        if (n < rowIdx) nextIgnored[n] = v;
        if (n > rowIdx) nextIgnored[n - 1] = v;
    });
    Object.entries(bomImportRaw.rowErrors || {}).forEach(([k, v]) => {
        const n = Number(k);
        if (n < rowIdx) nextErrors[n] = v;
        if (n > rowIdx) nextErrors[n - 1] = v;
    });
    bomImportRaw.ignoredRows = nextIgnored;
    bomImportRaw.rowErrors = nextErrors;
    rebuildBOMRowsFromRaw();
}

function clearBOMLotMapping(rowIdx) {
    const row = bomRows.find(r => r.source_row_index === rowIdx);
    if (!row) return;
    row.manual_lot = '';
    row.resolved_lot = '';
    row.resolution_source = 'unresolved';
    row.lot_allocations = [];
    row.bundle_enabled = false;
    renderBOMTable();
    debouncedResolveBOM();
}

function _bomRawLotCell(rowIdx) {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw') return '—';
    if (bomImportRaw.ignoredRows?.[rowIdx]) return '<span class="text-gray-400">—</span>';
    const validation = _validateBomColumnTypes();
    if (!validation.ok) return '<span class="text-gray-400 text-xs">Выберите P/N и Кол-во</span>';
    const map = bomRows.find(r => r.source_row_index === rowIdx);
    if (!map) {
        const err = bomImportRaw.rowErrors?.[rowIdx];
        return err ? `<span class="text-red-600 text-xs">${escapeHtml(err)}</span>` : '<span class="text-gray-400">—</span>';
    }

    const cart = window._currentCart || [];
    const cartMap = {};
    cart.forEach(item => { cartMap[item.lot_name] = item.quantity; });
    const isUnresolved = !map.resolved_lot || map.resolution_source === 'unresolved';
    const cartQty = map.resolved_lot ? (cartMap[map.resolved_lot] ?? null) : null;
    const qtyMismatch = cartQty !== null && cartQty !== map.quantity * _getRowLotQtyPerPN(map);
    const notInCart = map.resolved_lot && cartQty === null;

    if (isUnresolved) {
        const val = map.manual_lot || '';
        return `<div class="autocomplete-wrapper relative"><input type="text" placeholder="Введите артикул..."
            value="${escapeHtml(val)}"
            class="w-full min-w-28 px-2 py-1 border rounded text-xs font-mono"
            onfocus="showAutocompleteBOM(${rowIdx}, this)"
            oninput="filterAutocompleteBOM(${rowIdx}, this.value); setBOMManualLotDraft(${rowIdx}, this.value, this)"
            onkeydown="handleAutocompleteKeyBOM(event, ${rowIdx})"></div>
            ${renderBOMLotAllocationsEditor(rowIdx)}`;
    }
    let suffix = '';
    if (qtyMismatch) suffix = ` <span class="text-yellow-600 text-xs">≠est(${cartQty})</span>`;
    else if (notInCart) suffix = ` <span class="text-orange-500 text-xs">новый</span>`;
    return `<div><span class="font-mono text-xs">${escapeHtml(map.resolved_lot)}</span>${suffix}</div>${renderBOMLotAllocationsEditor(rowIdx)}`;
}

function commitBOMManualLot(rowIdx, el) {
    const row = bomRows.find(r => r.source_row_index === rowIdx);
    if (!row || !el) return;
    const v = (el.value || '').trim();
    if (!v) {
        row.manual_lot = '';
        el.value = '';
        debouncedResolveBOM();
        return;
    }
    if (!_bomLotValid(v)) {
        el.value = row.manual_lot || '';
        el.classList.add('border-red-400', 'bg-red-50');
        return;
    }
    row.manual_lot = v;
    el.classList.remove('border-red-400', 'bg-red-50');
    debouncedResolveBOM();
}

function setBOMManualLotDraft(rowIdx, value, el) {
    const row = bomRows.find(r => r.source_row_index === rowIdx);
    if (!row) return;
    row.manual_lot = value;
    if (el) {
        const invalid = value && !_bomLotValid(value);
        el.classList.toggle('border-red-400', !!invalid);
        el.classList.toggle('bg-red-50', !!invalid);
    }
}

function _getRowAllocations(row) {
    const list = Array.isArray(row?.lot_allocations) ? row.lot_allocations : [];
    return list.map(a => ({
        lot_name: (a?.lot_name || '').trim(),
        quantity: Math.max(1, parseInt(a?.quantity, 10) || 1)
    }));
}
function _getRowLotQtyPerPN(row) {
    const q = parseInt(row?.lot_qty_per_pn, 10);
    return (Number.isFinite(q) && q > 0) ? q : 1;
}
function _getRowBaseLot(row) {
    if (row?.resolved_lot) return row.resolved_lot;
    const manual = (row?.manual_lot || '').trim();
    if (manual && _bomLotValid(manual)) return manual;
    return '';
}
function _getRowCanonicalLotMappings(row) {
    const out = [];
    const baseLot = _getRowBaseLot(row);
    if (baseLot && _bomLotValid(baseLot)) {
        out.push({
            lot_name: baseLot,
            quantity_per_pn: _getRowLotQtyPerPN(row)
        });
    }
    _getRowAllocations(row)
        .filter(a => a.lot_name && _bomLotValid(a.lot_name) && a.quantity >= 1)
        .forEach(a => {
            out.push({
                lot_name: a.lot_name,
                quantity_per_pn: a.quantity
            });
        });
    return out;
}

function _rowHasValidAllocations(row) {
    const list = _getRowAllocations(row).filter(a => a.lot_name && _bomLotValid(a.lot_name) && a.quantity >= 1);
    return list.length > 0;
}
function _rowBundleEnabled(row) {
    return !!row?.bundle_enabled || _getRowAllocations(row).length > 0;
}

function _ensureRowAllocations(row) {
    if (!row) return;
    if (!Array.isArray(row.lot_allocations)) row.lot_allocations = [];
}

function renderBOMLotAllocationsEditor(rowIdx) {
    const row = bomRows.find(r => r.source_row_index === rowIdx) || bomRows[rowIdx];
    if (!row) return '';
    _ensureRowAllocations(row);
    if (!_rowBundleEnabled(row)) return '';
    const allocs = _getRowAllocations(row);
    const rowsHtml = allocs.map((a, i) => {
        const invalidLot = a.lot_name && !_bomLotValid(a.lot_name);
        return `<div class="${i === 0 ? '' : 'mt-1'}">
            <input type="text" value="${escapeHtml(a.lot_name)}" placeholder="LOT"
                   class="w-full min-w-0 px-2 py-0.5 border rounded text-[11px] ${invalidLot ? 'border-red-400 bg-red-50' : ''}"
                   list="lot-autocomplete-list"
                   oninput="setBOMAllocationDraft(${rowIdx}, ${i}, 'lot_name', this.value, this)"
                   onchange="commitBOMAllocation(${rowIdx}, ${i}, 'lot_name', this)"
                   onblur="commitBOMAllocation(${rowIdx}, ${i}, 'lot_name', this)">
        </div>`;
    }).join('');
    return `<div class="mt-1 pt-1 border-t border-gray-100 flex flex-col gap-1">${rowsHtml}</div>`;
}

function renderBOMLotAllocationsQtyEditor(rowIdx) {
    const row = bomRows.find(r => r.source_row_index === rowIdx) || bomRows[rowIdx];
    if (!row) return '';
    _ensureRowAllocations(row);
    if (!_rowBundleEnabled(row)) return '';
    const allocs = _getRowAllocations(row);
    const baseQtyHtml = `<div class="flex items-center gap-1">
            <input type="number" min="1" step="1" value="${_getRowLotQtyPerPN(row)}"
                   class="w-16 px-1 py-0.5 border rounded text-[11px] text-right"
                   oninput="setBOMSingleLotQtyDraft(${rowIdx}, this.value)"
                   onchange="commitBOMSingleLotQty(${rowIdx}, this)"
                   onblur="commitBOMSingleLotQty(${rowIdx}, this)">
            <span class="text-[10px] text-gray-500">/PN</span>
        </div>`;
    const rowsHtml = allocs.map((a, i) => `
        <div class="flex items-center gap-1">
            <input type="number" min="1" step="1" value="${a.quantity}"
                   class="w-16 px-1 py-0.5 border rounded text-[11px] text-right"
                   oninput="setBOMAllocationDraft(${rowIdx}, ${i}, 'quantity', this.value, this)"
                   onchange="commitBOMAllocation(${rowIdx}, ${i}, 'quantity', this)"
                   onblur="commitBOMAllocation(${rowIdx}, ${i}, 'quantity', this)">
            <span class="text-[10px] text-gray-500">/PN</span>
            <button type="button" class="text-[11px] text-gray-400 hover:text-red-600 px-1" title="Удалить LOT" onclick="removeBOMAllocation(${rowIdx}, ${i})">✕</button>
        </div>`).join('');
    return `<div class="mt-1 pt-1 border-t border-gray-100 flex flex-col gap-1">${baseQtyHtml}${rowsHtml}</div>`;
}

function _findBOMRowBySource(rowIdx) {
    return bomRows.find(r => r.source_row_index === rowIdx) || bomRows[rowIdx] || null;
}

function addBOMAllocation(rowIdx) {
    const row = _findBOMRowBySource(rowIdx);
    if (!row) return;
    _ensureRowAllocations(row);
    row.bundle_enabled = true;
    row.lot_allocations.push({ lot_name: row.resolved_lot || '', quantity: 1 });
    renderBOMTable();
    debouncedAutosaveBOM();
}

function removeBOMAllocation(rowIdx, allocIdx) {
    const row = _findBOMRowBySource(rowIdx);
    if (!row || !Array.isArray(row.lot_allocations)) return;
    row.lot_allocations.splice(allocIdx, 1);
    if (!row.lot_allocations.length) row.bundle_enabled = false;
    renderBOMTable();
    debouncedAutosaveBOM();
}

function setBOMAllocationDraft(rowIdx, allocIdx, field, value, el) {
    const row = _findBOMRowBySource(rowIdx);
    if (!row) return;
    _ensureRowAllocations(row);
    if (!row.lot_allocations[allocIdx]) row.lot_allocations[allocIdx] = { lot_name: '', quantity: 1 };
    if (field === 'lot_name') {
        row.lot_allocations[allocIdx].lot_name = value;
        if (el) {
            const invalid = value && !_bomLotValid(value);
            el.classList.toggle('border-red-400', !!invalid);
            el.classList.toggle('bg-red-50', !!invalid);
        }
    } else if (field === 'quantity') {
        row.lot_allocations[allocIdx].quantity = value;
    }
}

function setBOMSingleLotQtyDraft(rowIdx, value) {
    const row = _findBOMRowBySource(rowIdx);
    if (!row) return;
    row.lot_qty_per_pn = value;
}

function commitBOMSingleLotQty(rowIdx, el) {
    const row = _findBOMRowBySource(rowIdx);
    if (!row) return;
    row.lot_qty_per_pn = _getRowLotQtyPerPN({ lot_qty_per_pn: el?.value });
    if (el) el.value = String(row.lot_qty_per_pn);
    renderBOMTable();
    debouncedAutosaveBOM();
}

function commitBOMAllocation(rowIdx, allocIdx, field, el) {
    const row = _findBOMRowBySource(rowIdx);
    if (!row || !Array.isArray(row.lot_allocations) || !row.lot_allocations[allocIdx]) return;
    const a = row.lot_allocations[allocIdx];
    if (field === 'lot_name') {
        const v = (el?.value || '').trim();
        if (v && !_bomLotValid(v)) {
            if (el) {
                el.value = (a.lot_name && _bomLotValid(a.lot_name)) ? a.lot_name : '';
                el.classList.add('border-red-400', 'bg-red-50');
            }
            return;
        }
        a.lot_name = v;
        if (el) el.classList.remove('border-red-400', 'bg-red-50');
    } else if (field === 'quantity') {
        const q = parseInt(el?.value, 10);
        a.quantity = (Number.isFinite(q) && q > 0) ? q : 1;
        if (el) el.value = String(a.quantity);
    }
    renderBOMTable();
    debouncedAutosaveBOM();
}

let _rebuildBOMTimer = null;
function debouncedRebuildBOMFromRaw() {
    clearTimeout(_rebuildBOMTimer);
    _rebuildBOMTimer = setTimeout(() => rebuildBOMRowsFromRaw(), 350);
}

function rebuildBOMRowsFromRaw() {
    if (!bomImportRaw || bomImportRaw.mode !== 'raw') return;
    bomImportRaw.rowErrors = {};
    const validation = _validateBomColumnTypes();
    if (!validation.ok) {
        bomRows = [];
        _setBomUIError(validation.message);
        renderBOMTable();
        if (currentTopTab === 'pricing') renderPricingTab();
        return;
    }
    _setBomUIError('');

    const idx = validation.idx;
    const pnCol = idx.pn[0];
    const qtyCol = idx.qty[0];
    const priceCol = idx.price.length ? idx.price[0] : -1;
    const descCol = idx.description.length ? idx.description[0] : -1;

    const nextRows = [];
    bomImportRaw.rows.forEach((cols, rowIdx) => {
        if (bomImportRaw.ignoredRows?.[rowIdx]) return;
        const pn = ((cols[pnCol] || '') + '').trim();
        if (!pn) return;
        const qtyRaw = ((cols[qtyCol] || '') + '').trim();
        if (!/^\d+$/.test(qtyRaw) || parseInt(qtyRaw, 10) < 1) {
            bomImportRaw.rowErrors[rowIdx] = 'Некорректное кол-во';
            return;
        }
        const quantity = parseInt(qtyRaw, 10);
        const description = descCol !== -1 ? ((cols[descCol] || '') + '').trim() : '';
        const unit_price = priceCol !== -1 ? parsePastePrice(cols[priceCol] || '') : null;
        const total_price = unit_price != null ? unit_price * quantity : null;

        const prev = bomRows.find(r => r.source_row_index === rowIdx) || {};
        nextRows.push({
            sort_order: (nextRows.length + 1) * 10,
            vendor_pn: pn,
            quantity,
            description,
            unit_price,
            total_price,
            resolved_lot: prev.resolved_lot || '',
            resolution_source: prev.resolution_source || 'unresolved',
            manual_lot: (prev.manual_lot && _bomLotValid(prev.manual_lot)) ? prev.manual_lot : '',
            lot_qty_per_pn: _getRowLotQtyPerPN(prev),
            lot_allocations: _getRowAllocations(prev),
            bundle_enabled: _rowBundleEnabled(prev),
            source_row_index: rowIdx
        });
    });

    bomRows = nextRows;
    renderBOMTable();
    if (!bomRows.length) {
        if (currentTopTab === 'pricing') renderPricingTab();
        return;
    }
    debouncedResolveBOM();
}

async function resolveBOM() {
    if (!configUUID) {
        renderBOMTable();
        return;
    }
    const specPayload = bomRows.map(r => ({
        sort_order: r.sort_order,
        vendor_partnumber: r.vendor_pn,
        quantity: r.quantity,
        description: r.description,
        unit_price: r.unit_price,
        total_price: r.total_price,
        manual_lot_suggestion: (r.manual_lot && _bomLotValid(r.manual_lot)) ? r.manual_lot : '',
        lot_allocations: _getRowAllocations(r).filter(a => a.lot_name && _bomLotValid(a.lot_name) && a.quantity >= 1)
    }));

    try {
        const resp = await fetch(`/api/configs/${configUUID}/vendor-spec/resolve`, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({vendor_spec: specPayload})
        });
        if (!resp.ok) throw new Error(await resp.text());
        const data = await resp.json();
        if (data.resolved) {
            data.resolved.forEach((r, i) => {
                if (bomRows[i]) {
                    bomRows[i].resolved_lot = r.resolved_lot_name || '';
                    bomRows[i].resolution_source = r.resolution_source || 'unresolved';
                }
            });
        }

        const unseen = bomRows
            .filter(r => !r.resolved_lot || r.resolution_source === 'unresolved')
            .map(r => ({ partnumber: r.vendor_pn, description: r.description || '', ignored: false }));
        let ignoredSeen = [];
        if (bomImportRaw && bomImportRaw.mode === 'raw') {
            const validation = _validateBomColumnTypes();
            if (validation.ok) {
                const pnCol = validation.idx.pn[0];
                const descCol = validation.idx.description.length ? validation.idx.description[0] : -1;
                ignoredSeen = (bomImportRaw.rows || [])
                    .map((cols, rowIdx) => ({ cols, rowIdx }))
                    .filter(x => bomImportRaw.ignoredRows?.[x.rowIdx])
                    .map(({ cols }) => ({
                        partnumber: ((cols[pnCol] || '') + '').trim(),
                        description: descCol !== -1 ? ((cols[descCol] || '') + '').trim() : '',
                        ignored: true
                    }))
                    .filter(x => x.partnumber);
            }
        }
        const partnumberSeenPayload = [...unseen, ...ignoredSeen];
        if (partnumberSeenPayload.length) {
            fetch('/api/sync/partnumber-seen', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({items: partnumberSeenPayload})
            }).catch(() => {});
        }
    } catch (e) {
        console.warn('Resolution failed:', e);
    }

    renderBOMTable();
    debouncedAutosaveBOM();
}

function _renderBOMParsedTable() {
    _setBomUIError('');
    _ensureBomDatalist();
    const thead = document.getElementById('bom-table-head');
    const tbody = document.getElementById('bom-table-body');
    thead.innerHTML = `
        <tr>
            <th class="px-3 py-2 text-left border-b">PN вендора</th>
            <th class="px-3 py-2 text-right border-b">Кол-во</th>
            <th class="px-3 py-2 text-left border-b">Описание</th>
            <th class="px-3 py-2 text-right border-b">Цена ед.</th>
            <th class="px-3 py-2 text-right border-b">Итого</th>
            <th class="px-3 py-2 text-left border-b">LOT</th>
        </tr>`;
    const cart = window._currentCart || [];
    const cartMap = {};
    cart.forEach(item => { cartMap[item.lot_name] = item.quantity; });
    let unresolved = 0, mismatches = 0;
    tbody.innerHTML = '';

    bomRows.forEach((row, idx) => {
        const tr = document.createElement('tr');
        const isUnresolved = !row.resolved_lot || row.resolution_source === 'unresolved';
        const cartQty = row.resolved_lot ? (cartMap[row.resolved_lot] ?? null) : null;
        const qtyMismatch = cartQty !== null && cartQty !== row.quantity * _getRowLotQtyPerPN(row);
        const notInCart = row.resolved_lot && cartQty === null;
        if (isUnresolved) unresolved++;
        if (qtyMismatch || notInCart) mismatches++;
        tr.className = isUnresolved ? 'bg-red-50' : (qtyMismatch ? 'bg-yellow-50' : (notInCart ? 'bg-orange-50' : ''));

        let lotCell = '';
        if (isUnresolved) {
            lotCell = `<div class="autocomplete-wrapper relative"><input type="text" placeholder="Введите артикул..."
                value="${escapeHtml(row.manual_lot || '')}"
                class="w-full px-2 py-1 border rounded text-sm font-mono focus:ring-1 focus:ring-blue-400"
                onfocus="showAutocompleteBOM(${row.source_row_index}, this)"
                oninput="filterAutocompleteBOM(${row.source_row_index}, this.value); bomRows.find(r=>r.source_row_index===${row.source_row_index}).manual_lot=this.value;"
                onkeydown="handleAutocompleteKeyBOM(event, ${row.source_row_index})"></div>${renderBOMLotAllocationsEditor(idx)}`;
        } else {
            let suffix = '';
            if (qtyMismatch) suffix = ` <span class="text-yellow-600 text-xs">≠est(${cartQty})</span>`;
            else if (notInCart) suffix = ` <span class="text-orange-500 text-xs">новый</span>`;
            lotCell = `<div><span class="font-mono text-xs">${row.resolved_lot}</span>${suffix}</div>${renderBOMLotAllocationsEditor(idx)}`;
        }

        tr.innerHTML = `
            <td class="px-3 py-1.5 font-mono text-xs">${escapeHtml(row.vendor_pn)}</td>
            <td class="px-3 py-1.5 text-right">${row.quantity}</td>
            <td class="px-3 py-1.5 text-gray-600 text-xs">${escapeHtml(row.description || '')}</td>
            <td class="px-3 py-1.5 text-right text-xs">${row.unit_price != null ? formatCurrency(row.unit_price) : '—'}</td>
            <td class="px-3 py-1.5 text-right text-xs">${row.total_price != null ? formatCurrency(row.total_price) : '—'}</td>
            <td class="px-3 py-1.5">${lotCell}</td>`;
        tbody.appendChild(tr);
    });
    document.getElementById('bom-stats').textContent = `Строк: ${bomRows.length}  |  Не сопоставлено: ${unresolved}  |  Расхождений: ${mismatches}`;
}

function _renderBOMRawTable() {
    _ensureBomDatalist();
    const thead = document.getElementById('bom-table-head');
    const tbody = document.getElementById('bom-table-body');
    const colCount = _bomRawColCount();
    const validation = _validateBomColumnTypes();
    const allRows = bomImportRaw?.rows || [];

    const headCols = Array.from({length: colCount}, (_, i) => {
        const colType = bomImportRaw.columnTypes[i] || 'ignore';
        return `<th class="px-2 py-2 text-left border-b align-top min-w-28">
            <select class="w-full text-xs border rounded px-1 py-1 ${_bomRawHeaderWidthClass(colType)} max-w-full" onchange="onBOMColumnTypeChange(${i}, this.value)">
                ${BOM_COL_TYPES.map(t => `<option value="${t.value}" ${bomImportRaw.columnTypes[i] === t.value ? 'selected' : ''}>${t.label}</option>`).join('')}
            </select>
        </th>`;
    }).join('');
    thead.innerHTML = `<tr>${headCols}<th class="px-2 py-2 text-left border-b min-w-72 w-80">LOT</th><th class="px-2 py-2 text-left border-b w-28 min-w-28">LOT в 1 PN</th><th class="w-12 px-1 py-2 text-center border-b"></th></tr>`;

    tbody.innerHTML = '';
    let unresolved = 0, mismatches = 0;
    const cart = window._currentCart || [];
    const cartMap = {};
    cart.forEach(item => { cartMap[item.lot_name] = item.quantity; });

    allRows.forEach((cols, rowIdx) => {
        const tr = document.createElement('tr');
        const ignored = !!bomImportRaw.ignoredRows?.[rowIdx];
        const parsed = bomRows.find(r => r.source_row_index === rowIdx);
        const rowErr = bomImportRaw.rowErrors?.[rowIdx];
        if (ignored) tr.className = 'opacity-60 bg-gray-50';
        else if (rowErr) tr.className = 'bg-red-50';
        else if (parsed) {
            const isUnresolved = !parsed.resolved_lot || parsed.resolution_source === 'unresolved';
            const cartQty = parsed.resolved_lot ? (cartMap[parsed.resolved_lot] ?? null) : null;
            const qtyMismatch = cartQty !== null && cartQty !== parsed.quantity * _getRowLotQtyPerPN(parsed);
            const notInCart = parsed.resolved_lot && cartQty === null;
            if (isUnresolved) unresolved++;
            if (qtyMismatch || notInCart) mismatches++;
            if (isUnresolved) tr.className = 'bg-red-50';
            else if (qtyMismatch) tr.className = 'bg-yellow-50';
            else if (notInCart) tr.className = 'bg-orange-50';
        }

        const rawCells = cols.map((v, colIdx) => {
            const t = bomImportRaw.columnTypes[colIdx] || 'ignore';
            const widthCls = _bomRawCellWidthClass(t);
            return `<td class="px-2 py-1.5 border-b align-top ${widthCls}">
            <div contenteditable="true" class="min-h-6 text-xs outline-none focus:ring-1 focus:ring-blue-300 rounded px-1 ${t === 'qty' ? 'text-right' : ''}"
                 onblur="onBOMRawCellInput(${rowIdx}, ${colIdx}, this.textContent)"
                 onkeydown="if(event.key==='Enter'){event.preventDefault(); this.blur();}">${escapeHtml(v || '')}</div>
        </td>`;
        }).join('');
        const lotHtml = _bomRawLotCell(rowIdx);
        const lotQtyHtml = parsed
            ? (_rowBundleEnabled(parsed)
                ? renderBOMLotAllocationsQtyEditor(rowIdx)
                : `<input type="number" min="1" step="1" value="${_getRowLotQtyPerPN(parsed)}"
                       class="w-20 px-1 py-0.5 border rounded text-xs text-right"
                       oninput="setBOMSingleLotQtyDraft(${rowIdx}, this.value)"
                       onchange="commitBOMSingleLotQty(${rowIdx}, this)"
                       onblur="commitBOMSingleLotQty(${rowIdx}, this)">`)
            : '<div class="text-xs text-gray-400 px-1">—</div>';
        const errHint = rowErr ? `<div class="text-[10px] text-red-600 mt-1">${escapeHtml(rowErr)}</div>` : '';
        tr.innerHTML = `
            ${rawCells}
            <td class="px-2 py-1.5 border-b align-top min-w-72 w-80">${lotHtml}${errHint}</td>
            <td class="px-2 py-1.5 border-b align-top w-28 min-w-28">${lotQtyHtml}</td>
            <td class="w-12 px-1 py-1 border-b text-center align-top whitespace-nowrap">
                <button type="button" title="Добавить LOT в bundle" onclick="addBOMAllocation(${rowIdx})" class="inline-block text-xs px-1 text-gray-400 hover:text-blue-600">+</button>
                <button type="button" title="${ignored ? 'Не игнорировать' : 'Игнорировать'}" onclick="toggleBOMRawRowIgnored(${rowIdx})" class="inline-block text-xs px-1 ${ignored ? 'text-blue-600' : 'text-gray-400 hover:text-gray-700'}">${ignored ? '◉' : '○'}</button>
                <button type="button" title="Снять сопоставление" onclick="clearBOMLotMapping(${rowIdx})" class="inline-block text-xs px-1 text-gray-400 hover:text-red-600">✕</button>
            </td>`;
        tbody.appendChild(tr);
    });

    const statsParts = [`Строк raw: ${allRows.length}`, `В BOM: ${bomRows.length}`];
    if (validation.ok) {
        statsParts.push(`Не сопоставлено: ${unresolved}`);
        statsParts.push(`Расхождений: ${mismatches}`);
    } else {
        statsParts.push('Ожидается выбор колонок');
    }
    document.getElementById('bom-stats').textContent = statsParts.join('  |  ');
}

function renderBOMTable() {
    document.getElementById('bom-table-container').classList.remove('hidden');
    if (bomImportRaw && bomImportRaw.mode === 'raw') _renderBOMRawTable();
    else _renderBOMParsedTable();
    if (currentTopTab === 'pricing') renderPricingTab();
}

let _resolveBOMTimer = null;
function debouncedResolveBOM() {
    clearTimeout(_resolveBOMTimer);
    _resolveBOMTimer = setTimeout(() => resolveBOM(), 500);
}

async function clearBOM() {
    if (bomRows.length && !confirm('Очистить BOM? Данные будут удалены с сервера.')) return;
    bomRows = [];
    bomImportRaw = null;
    _setBomUIError('');
    document.getElementById('bom-table-container').classList.add('hidden');
    document.getElementById('bom-paste-area').textContent = 'Нажмите сюда и вставьте из Excel (Ctrl+V)...';
    if (configUUID) {
        await fetch(`/api/configs/${configUUID}/vendor-spec`, {
            method: 'PUT',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({vendor_spec: []})
        });
    }
}

async function saveBOMInternal(showSuccessToast = true) {
    if (!configUUID || !bomRows.length) return;
    const spec = bomRows.map(r => ({
        sort_order: r.sort_order,
        vendor_partnumber: r.vendor_pn,
        quantity: r.quantity,
        description: r.description,
        unit_price: r.unit_price,
        total_price: r.total_price,
        lot_mappings: _getRowCanonicalLotMappings(r)
    }));
    const resp = await fetch(`/api/configs/${configUUID}/vendor-spec`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({vendor_spec: spec})
    });
    if (!resp.ok) throw new Error(await resp.text());
    if (showSuccessToast) showToast('BOM сохранён', 'success');
}

let _autosaveBOMTimer = null;
function debouncedAutosaveBOM() {
    clearTimeout(_autosaveBOMTimer);
    _autosaveBOMTimer = setTimeout(async () => {
        if (!configUUID || !bomRows.length) return;
        try {
            await saveBOMInternal(false);
        } catch (e) {
            console.warn('BOM autosave failed:', e);
        }
    }, 700);
}

async function saveBOM() {
    try {
        await saveBOMInternal(true);
    } catch (e) {
        showToast('Ошибка сохранения BOM: ' + e.message, 'error');
    }
}

async function applyBOMToEstimate() {
    if (!bomRows.length) return;
    if (bomRows.some(r => r.manual_lot && !_bomLotValid(r.manual_lot))) {
        alert('Есть несуществующий LOT в BOM. Выберите LOT из списка.');
        return;
    }
    const resolved = bomRows.filter(r => _getRowBaseLot(r) || _rowHasValidAllocations(r));
    if (!resolved.length) {
        alert('Нет сопоставленных строк. Сначала настройте LOT для всех позиций.');
        return;
    }

    // Aggregate quantities
    const lotMap = {};
    resolved.forEach(r => {
        const baseLot = _getRowBaseLot(r);
        const allocs = _getRowAllocations(r).filter(a => a.lot_name && _bomLotValid(a.lot_name) && a.quantity >= 1);
        if (baseLot) {
            if (!lotMap[baseLot]) lotMap[baseLot] = 0;
            lotMap[baseLot] += r.quantity * _getRowLotQtyPerPN(r);
        }
        if (allocs.length) {
            allocs.forEach(a => {
                if (!lotMap[a.lot_name]) lotMap[a.lot_name] = 0;
                lotMap[a.lot_name] += r.quantity * a.quantity;
            });
            return;
        }
        if (!baseLot) return;
    });

    const items = Object.entries(lotMap).map(([lot, qty]) => ({
        lot_name: lot,
        quantity: qty,
        unit_price: 0
    }));

    const mappedRowsCount = resolved.length;
    const unresolvedRowsCount = Math.max(0, bomRows.length - mappedRowsCount);
    const confirmText = [
        'Пересчитать Estimate?',
        `Сопоставлено строк BOM: ${mappedRowsCount}`,
        `Не сопоставлено строк BOM: ${unresolvedRowsCount}`,
        `Текущая корзина будет заменена ${items.length} LOT-позициями.`
    ].join('\n');
    if (!confirm(confirmText)) return;

    try {
        const resp = await fetch(`/api/configs/${configUUID}/vendor-spec/apply`, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({items})
        });
        if (!resp.ok) throw new Error(await resp.text());
        showToast('Estimate обновлён из BOM', 'success');
        // Reload the page to show updated estimate
        window.location.reload();
    } catch (e) {
        showToast('Ошибка: ' + e.message, 'error');
    }
}

// Load existing BOM on config load
async function loadVendorSpec(configUUID) {
    try {
        const resp = await fetch(`/api/configs/${configUUID}/vendor-spec`);
        if (!resp.ok) return;
        const data = await resp.json();
        if (data.vendor_spec && data.vendor_spec.length) {
            bomRows = data.vendor_spec.map((r, i) => ({
                _mappings: Array.isArray(r.lot_mappings) ? r.lot_mappings : [],
                sort_order: r.sort_order || (i + 1) * 10,
                vendor_pn: r.vendor_partnumber,
                quantity: r.quantity,
                description: r.description || '',
                unit_price: r.unit_price || null,
                total_price: r.total_price || null,
                resolved_lot: '',
                resolution_source: 'unresolved',
                manual_lot: '',
                lot_qty_per_pn: 1,
                lot_allocations: [],
                bundle_enabled: false,
                source_row_index: i
            })).map(row => {
                const mappings = Array.isArray(row._mappings) ? row._mappings : [];
                const validMappings = mappings
                    .map(m => ({
                        lot_name: (m?.lot_name || '').trim(),
                        quantity_per_pn: Math.max(1, parseInt(m?.quantity_per_pn, 10) || 1)
                    }))
                    .filter(m => m.lot_name);
                if (validMappings.length) {
                    row.resolved_lot = validMappings[0].lot_name;
                    row.manual_lot = validMappings[0].lot_name;
                    row.resolution_source = 'manual_suggestion';
                    row.lot_qty_per_pn = validMappings[0].quantity_per_pn;
                    row.lot_allocations = validMappings.slice(1).map(m => ({
                        lot_name: m.lot_name,
                        quantity: m.quantity_per_pn
                    }));
                    row.bundle_enabled = row.lot_allocations.length > 0;
                }
                delete row._mappings;
                return row;
            });

            // Reconstruct editable raw table from normalized vendor_spec (not original Excel paste).
            // Columns: Qty | P/N | Description | Price
            bomImportRaw = {
                mode: 'raw',
                rows: bomRows.map(r => ([
                    String(r.quantity ?? 1),
                    r.vendor_pn || '',
                    r.description || '',
                    r.unit_price != null ? String(r.unit_price) : ''
                ])),
                columnTypes: ['qty', 'pn', 'description', 'price'],
                ignoredRows: {},
                rowErrors: {},
                uiError: ''
            };
            renderBOMTable();
        }
    } catch (e) {
        console.warn('Failed to load vendor spec:', e);
    }
}

// ==================== ЦЕНООБРАЗОВАНИЕ ====================

async function renderPricingTab() {
    const tbodyBuy  = document.getElementById('pricing-body-buy');
    const tfootBuy  = document.getElementById('pricing-foot-buy');
    const tbodySale = document.getElementById('pricing-body-sale');
    const tfootSale = document.getElementById('pricing-foot-sale');

    const cart = window._currentCart || [];
    const compMap = {};
    (window._bomAllComponents || allComponents).forEach(c => { compMap[c.lot_name] = c; });
    const rowBaseLot = (row) => {
        if (row?.resolved_lot) return row.resolved_lot;
        if (row?.manual_lot && _bomLotValid(row.manual_lot)) return row.manual_lot;
        return '';
    };

    // Collect LOTs to price: from BOM rows (resolved) or from cart
    // Use cart quantity when available (source of truth); fall back to BOM-computed quantity.
    const _cartQtyMap = {};
    cart.forEach(item => { if (item?.lot_name) _cartQtyMap[item.lot_name] = item.quantity; });
    let itemsForPriceLevels = [];
    if (bomRows.length) {
        const seen = new Set();
        bomRows.forEach(row => {
            const baseLot = rowBaseLot(row);
            const allocs = _getRowAllocations(row).filter(a => a.lot_name && _bomLotValid(a.lot_name) && a.quantity >= 1);
            if (baseLot && !seen.has(baseLot)) {
                seen.add(baseLot);
                itemsForPriceLevels.push({ lot_name: baseLot, quantity: _cartQtyMap[baseLot] ?? (row.quantity * _getRowLotQtyPerPN(row)) });
            }
            if (allocs.length) {
                allocs.forEach(a => {
                    if (!seen.has(a.lot_name)) {
                        seen.add(a.lot_name);
                        itemsForPriceLevels.push({ lot_name: a.lot_name, quantity: _cartQtyMap[a.lot_name] ?? (row.quantity * a.quantity) });
                    }
                });
            }
        });
        cart.forEach(item => {
            if (!item?.lot_name || seen.has(item.lot_name)) return;
            seen.add(item.lot_name);
            itemsForPriceLevels.push({ lot_name: item.lot_name, quantity: item.quantity });
        });
    } else {
        itemsForPriceLevels = cart.map(item => ({ lot_name: item.lot_name, quantity: item.quantity }));
    }

    // Fetch fresh price levels for these LOTs
    const priceMap = {};
    if (itemsForPriceLevels.length) {
        try {
            const payload = {
                items: itemsForPriceLevels,
                pricelist_ids: Object.fromEntries(
                    Object.entries(selectedPricelistIds)
                        .filter(([, id]) => typeof id === 'number' && id > 0)
                )
            };
            const resp = await fetch('/api/quote/price-levels', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            if (resp.ok) {
                const data = await resp.json();
                (data.items || []).forEach(i => { priceMap[i.lot_name] = i; });
            }
        } catch(e) { /* silent */ }
    }

    // Sale uplift applied to estimate (default 1.3)
    const saleUplift = (() => {
        const v = parseDecimalInput(document.getElementById('pricing-uplift-sale')?.value || '');
        return v > 0 ? v : 1.3;
    })();
    const SALE_FIXED_MULT = 1.3;

    // Helper: returns unit prices from pricelist for a single LOT
    const _getUnitPrices = (pl) => ({
        estUnit:        (pl && pl.estimate_price  > 0) ? pl.estimate_price  : 0,
        warehouseUnit:  (pl && pl.warehouse_price  > 0) ? pl.warehouse_price  : null,
        competitorUnit: (pl && pl.competitor_price > 0) ? pl.competitor_price : null,
    });

    // ─── Build shared row data (unit prices for display, totals for math) ────
    // Each BOM row is exploded into per-LOT sub-rows; grouped by vendor PN via groupStart/groupSize.
    const cartQtyMap = {};
    cart.forEach(item => { if (item?.lot_name) cartQtyMap[item.lot_name] = item.quantity; });
    const _buildRows = () => {
        const result = [];
        const coveredLots = new Set();

        const _pushCartRow = (item, isEstOnly) => {
            const pl = priceMap[item.lot_name];
            const u  = _getUnitPrices(pl);
            const estUnit = u.estUnit > 0 ? u.estUnit : (item.unit_price || 0);
            result.push({
                lotCell: escapeHtml(item.lot_name), lotText: item.lot_name,
                vendorPN: null,
                desc: (compMap[item.lot_name] || {}).description || '',
                qty: item.quantity,
                estUnit, warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
                est:        estUnit * item.quantity,
                warehouse:  u.warehouseUnit  != null ? u.warehouseUnit  * item.quantity : null,
                competitor: u.competitorUnit != null ? u.competitorUnit * item.quantity : null,
                vendorOrig: null, vendorOrigUnit: null, isEstOnly,
                groupStart: true, groupSize: 1,
            });
        };

        if (!bomRows.length) {
            const sortedByCategory = [...cart].sort((a, b) => {
                const catA = (a.category || getCategoryFromLotName(a.lot_name)).toUpperCase();
                const catB = (b.category || getCategoryFromLotName(b.lot_name)).toUpperCase();
                return (categoryOrderMap[catA] || 9999) - (categoryOrderMap[catB] || 9999);
            });
            sortedByCategory.forEach(item => { _pushCartRow(item, false); coveredLots.add(item.lot_name); });
            return { result, coveredLots };
        }

        bomRows.forEach(row => {
            const baseLot = rowBaseLot(row);
            const allocs  = _getRowAllocations(row).filter(a => a.lot_name && _bomLotValid(a.lot_name) && a.quantity >= 1);
            if (baseLot) coveredLots.add(baseLot);
            allocs.forEach(a => coveredLots.add(a.lot_name));

            const vendorOrigUnit = row.unit_price != null ? row.unit_price
                : (row.total_price != null && row.quantity > 0 ? row.total_price / row.quantity : null);
            const vendorOrig = row.total_price != null ? row.total_price
                : (row.unit_price != null ? row.unit_price * row.quantity : null);
            const desc = row.description || (baseLot ? ((compMap[baseLot] || {}).description || '') : '');

            // Build per-LOT sub-rows
            const subRows = [];
            if (baseLot) {
                const u = _getUnitPrices(priceMap[baseLot]);
                const lotQty = _getRowLotQtyPerPN(row);
                const qty = cartQtyMap[baseLot] ?? (row.quantity * lotQty);
                subRows.push({
                    lotCell: escapeHtml(baseLot), lotText: baseLot, qty,
                    estUnit: u.estUnit > 0 ? u.estUnit : 0,
                    warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
                    est:        u.estUnit > 0 ? u.estUnit * qty : 0,
                    warehouse:  u.warehouseUnit  != null ? u.warehouseUnit  * qty : null,
                    competitor: u.competitorUnit != null ? u.competitorUnit * qty : null,
                });
            }
            allocs.forEach(a => {
                const u = _getUnitPrices(priceMap[a.lot_name]);
                const qty = cartQtyMap[a.lot_name] ?? (row.quantity * a.quantity);
                subRows.push({
                    lotCell: escapeHtml(a.lot_name), lotText: a.lot_name, qty,
                    estUnit: u.estUnit > 0 ? u.estUnit : 0,
                    warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
                    est:        u.estUnit > 0 ? u.estUnit * qty : 0,
                    warehouse:  u.warehouseUnit  != null ? u.warehouseUnit  * qty : null,
                    competitor: u.competitorUnit != null ? u.competitorUnit * qty : null,
                });
            });

            if (!subRows.length) {
                result.push({
                    lotCell: '<span class="text-red-500">н/д</span>', lotText: '',
                    vendorPN: row.vendor_pn, desc, qty: row.quantity,
                    estUnit: 0, warehouseUnit: null, competitorUnit: null,
                    est: 0, warehouse: null, competitor: null,
                    vendorOrig, vendorOrigUnit, isEstOnly: false,
                    groupStart: true, groupSize: 1,
                });
                return;
            }

            const groupSize = subRows.length;
            subRows.forEach((sub, idx) => {
                result.push({
                    lotCell: sub.lotCell, lotText: sub.lotText,
                    vendorPN: row.vendor_pn, desc,
                    qty: sub.qty,
                    estUnit: sub.estUnit, warehouseUnit: sub.warehouseUnit, competitorUnit: sub.competitorUnit,
                    est: sub.est, warehouse: sub.warehouse, competitor: sub.competitor,
                    vendorOrig:     idx === 0 ? vendorOrig     : null,
                    vendorOrigUnit: idx === 0 ? vendorOrigUnit : null,
                    isEstOnly: false,
                    groupStart: idx === 0,
                    groupSize:  idx === 0 ? groupSize : 0,
                });
            });
        });

        // Estimate-only LOTs (cart items not covered by BOM)
        cart.forEach(item => {
            if (!item?.lot_name || coveredLots.has(item.lot_name)) return;
            _pushCartRow(item, true);
            coveredLots.add(item.lot_name);
        });

        return { result, coveredLots };
    };

    const { result: rowData } = _buildRows();

    // ─── Populate Buy table ──────────────────────────────────────────────────
    tbodyBuy.innerHTML = '';
    if (!rowData.length) {
        tbodyBuy.innerHTML = '<tr><td colspan="8" class="px-3 py-8 text-center text-gray-400">Нет данных для отображения</td></tr>';
        tfootBuy.classList.add('hidden');
    } else {
        let totEst = 0, totWh = 0, totComp = 0, totVendor = 0;
        let hasEst = false, hasWh = false, hasComp = false, hasVendor = false;
        let cntWh = 0, cntComp = 0;
        rowData.forEach(r => {
            const tr = document.createElement('tr');
            tr.classList.add('pricing-row-buy');
            if (r.isEstOnly) tr.classList.add('bg-blue-50');
            tr.dataset.est = r.est;
            tr.dataset.qty = r.qty;
            tr.dataset.vendorOrig = r.vendorOrig != null ? r.vendorOrig : '';
            tr.dataset.vendorOrigUnit = r.vendorOrigUnit != null ? r.vendorOrigUnit : '';
            tr.dataset.groupStart = r.groupStart ? 'true' : 'false';
            tr.dataset.vendorPn = r.vendorPN || '';
            tr.dataset.desc = r.desc;
            tr.dataset.lot = r.lotText;
            if (r.est > 0)            { totEst    += r.est;        hasEst    = true; }
            if (r.warehouse  != null) { totWh     += r.warehouse;  hasWh     = true; cntWh++;  }
            if (r.competitor != null) { totComp   += r.competitor; hasComp   = true; cntComp++; }
            if (r.vendorOrig != null) { totVendor += r.vendorOrig; hasVendor = true; }
            const borderTop = r.groupStart ? 'border-t border-gray-200' : '';
            const pnDescHtml = r.groupStart ? (() => {
                const rs = r.groupSize > 1 ? ` rowspan="${r.groupSize}"` : '';
                return `<td${rs} class="px-3 py-1.5 font-mono text-xs border-t border-gray-200 align-top ${r.vendorPN == null ? 'text-gray-400' : ''}">${r.vendorPN != null ? escapeHtml(r.vendorPN) : '—'}</td>
                <td${rs} class="px-3 py-1.5 text-xs text-gray-500 truncate max-w-xs border-t border-gray-200 align-top">${escapeHtml(r.desc)}</td>`;
            })() : '';
            tr.innerHTML = `
                ${pnDescHtml}
                <td class="px-3 py-1.5 text-xs ${borderTop}">${r.lotCell}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.qty}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.estUnit > 0 ? formatCurrency(r.estUnit) : '—'}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.warehouseUnit != null ? formatCurrency(r.warehouseUnit) : '—'}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.competitorUnit != null ? formatCurrency(r.competitorUnit) : '—'}</td>
                <td class="px-3 py-1.5 text-right text-xs pricing-vendor-price-buy ${borderTop} ${r.vendorOrigUnit == null ? 'text-gray-400' : ''}">${r.vendorOrigUnit != null ? formatCurrency(r.vendorOrigUnit) : '—'}</td>
            `;
            tbodyBuy.appendChild(tr);
        });
        document.getElementById('pricing-total-buy-estimate').textContent  = hasEst    ? formatCurrency(totEst)    : '—';
        document.getElementById('pricing-total-buy-vendor').textContent     = hasVendor ? formatCurrency(totVendor) : '—';
        _setPartialTotal('pricing-total-buy-warehouse',  hasWh,   totWh,   cntWh,   rowData.length);
        _setPartialTotal('pricing-total-buy-competitor', hasComp, totComp, cntComp, rowData.length);
        tfootBuy.classList.remove('hidden');
    }

    // ─── Populate Sale table ─────────────────────────────────────────────────
    tbodySale.innerHTML = '';
    if (!rowData.length) {
        tbodySale.innerHTML = '<tr><td colspan="8" class="px-3 py-8 text-center text-gray-400">Нет данных для отображения</td></tr>';
        tfootSale.classList.add('hidden');
    } else {
        let totEst = 0, totWh = 0, totComp = 0;
        let hasEst = false, hasWh = false, hasComp = false;
        let cntWh = 0, cntComp = 0;
        rowData.forEach(r => {
            const tr = document.createElement('tr');
            tr.classList.add('pricing-row-sale');
            if (r.isEstOnly) tr.classList.add('bg-blue-50');
            const saleEstUnit  = r.estUnit > 0             ? r.estUnit  * saleUplift      : 0;
            const saleWhUnit   = r.warehouseUnit  != null  ? r.warehouseUnit  * SALE_FIXED_MULT : null;
            const saleCompUnit = r.competitorUnit != null  ? r.competitorUnit * SALE_FIXED_MULT : null;
            const saleEstTotal  = saleEstUnit  * r.qty;
            const saleWhTotal   = saleWhUnit   != null ? saleWhUnit   * r.qty : null;
            const saleCompTotal = saleCompUnit != null ? saleCompUnit * r.qty : null;
            tr.dataset.estSale = saleEstTotal;
            tr.dataset.qty = r.qty;
            tr.dataset.groupStart = r.groupStart ? 'true' : 'false';
            tr.dataset.vendorPn = r.vendorPN || '';
            tr.dataset.desc = r.desc;
            tr.dataset.lot = r.lotText;
            if (saleEstTotal  > 0)     { totEst  += saleEstTotal;  hasEst  = true; }
            if (saleWhTotal   != null) { totWh   += saleWhTotal;   hasWh   = true; cntWh++;  }
            if (saleCompTotal != null) { totComp += saleCompTotal; hasComp = true; cntComp++; }
            const borderTop = r.groupStart ? 'border-t border-gray-200' : '';
            const pnDescHtml = r.groupStart ? (() => {
                const rs = r.groupSize > 1 ? ` rowspan="${r.groupSize}"` : '';
                return `<td${rs} class="px-3 py-1.5 font-mono text-xs border-t border-gray-200 align-top ${r.vendorPN == null ? 'text-gray-400' : ''}">${r.vendorPN != null ? escapeHtml(r.vendorPN) : '—'}</td>
                <td${rs} class="px-3 py-1.5 text-xs text-gray-500 truncate max-w-xs border-t border-gray-200 align-top">${escapeHtml(r.desc)}</td>`;
            })() : '';
            tr.innerHTML = `
                ${pnDescHtml}
                <td class="px-3 py-1.5 text-xs ${borderTop}">${r.lotCell}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.qty}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${saleEstUnit  > 0    ? formatCurrency(saleEstUnit)  : '—'}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${saleWhUnit   != null ? formatCurrency(saleWhUnit)   : '—'}</td>
                <td class="px-3 py-1.5 text-right text-xs ${borderTop}">${saleCompUnit != null ? formatCurrency(saleCompUnit) : '—'}</td>
                <td class="px-3 py-1.5 text-right text-xs pricing-vendor-price-sale ${borderTop} text-gray-400">—</td>
            `;
            tbodySale.appendChild(tr);
        });
        document.getElementById('pricing-total-sale-estimate').textContent = hasEst ? formatCurrency(totEst) : '—';
        document.getElementById('pricing-total-sale-vendor').textContent    = '—';
        _setPartialTotal('pricing-total-sale-warehouse',  hasWh,   totWh,   cntWh,   rowData.length);
        _setPartialTotal('pricing-total-sale-competitor', hasComp, totComp, cntComp, rowData.length);
        tfootSale.classList.remove('hidden');
    }

    // Restore custom prices after re-render
    applyCustomPrice('buy');
    applyCustomPrice('sale');
}

// ─── Pricing helpers ─────────────────────────────────────────────────────────

// Sets a footer total cell. If has prices but coverage < totalRows, marks red with a hover asterisk.
function _setPartialTotal(elId, has, total, count, totalRows) {
    const el = document.getElementById(elId);
    if (!el) return;
    el.className = el.className.replace(/\btext-red-\d+\b/g, '').trim();
    if (!has) { el.textContent = '—'; return; }
    if (count < totalRows) {
        el.innerHTML = `<span class="text-red-600">${formatCurrency(total)}</span> <span class="text-red-400 cursor-help" title="Цены указаны не для всех позиций (${count} из ${totalRows})">*</span>`;
    } else {
        el.textContent = formatCurrency(total);
    }
}

function parseDecimalInput(raw) {
    if (raw == null) return 0;
    const cleaned = String(raw).trim().replace(/\s/g, '').replace(',', '.');
    const n = parseFloat(cleaned);
    return Number.isFinite(n) ? n : 0;
}

function formatUpliftInput(value) {
    if (!Number.isFinite(value) || value <= 0) return '';
    return value.toFixed(4).replace('.', ',');
}

function _getPricingEstimateTotal(table) {
    const attr = table === 'sale' ? 'estSale' : 'est';
    const cls  = table === 'sale' ? 'pricing-row-sale' : 'pricing-row-buy';
    let total = 0;
    document.querySelectorAll(`#pricing-body-${table} tr.${cls}`).forEach(tr => {
        total += parseFloat(tr.dataset[attr]) || 0;
    });
    return total;
}

// Apply custom (own) price proportionally to Ручная цена column.
// table: 'buy' | 'sale'
function applyCustomPrice(table) {
    const inputId   = `pricing-custom-price-${table}`;
    const totalElId = `pricing-total-${table}-vendor`;
    const rowClass  = `pricing-row-${table}`;
    const cellClass = `.pricing-vendor-price-${table}`;
    const estAttr   = table === 'sale' ? 'estSale' : 'est';
    const origAttr  = table === 'buy'  ? 'vendorOrig' : null;

    const customPrice  = parseFloat(document.getElementById(inputId)?.value) || 0;
    const estimateTotal = _getPricingEstimateTotal(table);
    const rows       = document.querySelectorAll(`#pricing-body-${table} tr.${rowClass}`);
    const vendorCells = document.querySelectorAll(`#pricing-body-${table} ${cellClass}`);
    const totalVendorEl = document.getElementById(totalElId);

    const _pctLabel = (custom, est) => {
        if (est <= 0) return '';
        const pct = ((est - custom) / est * 100);
        const sign = pct >= 0 ? '-' : '+';
        return ` (${sign}${Math.abs(pct).toFixed(1).replace('.', ',')}%)`;
    };
    const _pctClass = (custom, est) => custom <= est ? 'text-green-600' : 'text-red-600';

    if (customPrice > 0 && estimateTotal > 0) {
        let assigned = 0;
        rows.forEach((tr, i) => {
            const rowEst = parseFloat(tr.dataset[estAttr]) || 0;
            const qty    = Math.max(1, parseFloat(tr.dataset.qty) || 1);
            const cell = vendorCells[i];
            if (!cell) return;
            let share;
            if (i === rows.length - 1) {
                share = customPrice - assigned;
            } else {
                share = Math.round((rowEst / estimateTotal) * customPrice * 100) / 100;
                assigned += share;
            }
            cell.textContent = formatCurrency(share / qty);
            cell.className = cell.className.replace(/\btext-(?:gray|green|red|blue)-\d+\b/g, '').trim();
            cell.classList.add(rowEst > 0 ? _pctClass(share, rowEst) : 'text-blue-700');
        });
        const pctStr = _pctLabel(customPrice, estimateTotal);
        totalVendorEl.textContent = formatCurrency(customPrice) + pctStr;
        totalVendorEl.className = totalVendorEl.className.replace(/\btext-(?:green|red)-\d+\b/g, '').trim();
        totalVendorEl.classList.add(_pctClass(customPrice, estimateTotal));
    } else {
        // Restore originals
        rows.forEach((tr, i) => {
            const cell = vendorCells[i];
            if (!cell) return;
            cell.className = cell.className.replace(/\btext-(?:gray|green|red|blue)-\d+\b/g, '').trim();
            if (origAttr && tr.dataset.vendorOrigUnit !== '') {
                cell.textContent = formatCurrency(parseFloat(tr.dataset.vendorOrigUnit));
            } else {
                cell.textContent = '—';
                cell.classList.add('text-gray-400');
            }
        });
        // Recompute total from originals (buy) or clear (sale)
        if (origAttr) {
            let origTotal = 0; let hasOrig = false;
            rows.forEach(tr => { if (tr.dataset[origAttr] !== '') { origTotal += parseFloat(tr.dataset[origAttr]) || 0; hasOrig = true; } });
            totalVendorEl.textContent = hasOrig ? formatCurrency(origTotal) : '—';
        } else {
            // sale: reset to — already handled above
            totalVendorEl.textContent = '—';
        }
        totalVendorEl.className = totalVendorEl.className.replace(/\btext-(?:green|red)-\d+\b/g, '').trim();
    }
}

function onBuyCustomPriceInput() {
    applyCustomPrice('buy');
    triggerAutoSave();
}

function onSaleCustomPriceInput() {
    applyCustomPrice('sale');
    triggerAutoSave();
}

function onSaleMarkupInput() {
    renderPricingTab();
    triggerAutoSave();
}

function setPricingCustomPriceFromVendor() {
    // Fill Ручная цена in Buy table from BOM vendor totals
    const rows = document.querySelectorAll('#pricing-body-buy tr.pricing-row-buy');
    const vendorCells = document.querySelectorAll('#pricing-body-buy .pricing-vendor-price-buy');
    let total = 0;
    let hasAny = false;
    rows.forEach((tr, i) => {
        const cell = vendorCells[i];
        if (!cell) return;
        const origUnit = tr.dataset.vendorOrigUnit;
        const origTotal = tr.dataset.vendorOrig;
        if (origUnit !== '') {
            cell.textContent = formatCurrency(parseFloat(origUnit));
            cell.className = cell.className.replace(/\btext-(?:gray|green|red|blue)-\d+\b/g, '').trim();
            total += parseFloat(origTotal) || 0;
            hasAny = true;
        } else {
            cell.textContent = '—';
            cell.className = cell.className.replace(/\btext-(?:green|red|blue)-\d+\b/g, '').trim();
            cell.classList.add('text-gray-400');
        }
    });
    const estimateTotal = _getPricingEstimateTotal('buy');
    const totalEl = document.getElementById('pricing-total-buy-vendor');
    if (hasAny) {
        document.getElementById('pricing-custom-price-buy').value = total.toFixed(2);
        const pct = estimateTotal > 0 ? ` (-${((estimateTotal - total) / estimateTotal * 100).toFixed(1).replace('.', ',')}%)` : '';
        totalEl.textContent = formatCurrency(total) + pct;
        totalEl.className = totalEl.className.replace(/\btext-(?:green|red)-\d+\b/g, '').trim();
        totalEl.classList.add(total <= estimateTotal ? 'text-green-600' : 'text-red-600');
    } else {
        document.getElementById('pricing-custom-price-buy').value = '';
        totalEl.textContent = '—';
    }
}

async function exportPricingCSV(table) {
    if (!configUUID) { showToast('Сохраните конфигурацию перед экспортом', 'error'); return; }
    const basis = table === 'sale' ? 'ddp' : 'fob';
    const manualInputId = table === 'sale' ? 'pricing-custom-price-sale' : 'pricing-custom-price-buy';
    const manualPrice = parseDecimalInput(document.getElementById(manualInputId)?.value || '');
    try {
        const resp = await fetch(`/api/configs/${configUUID}/export/pricing`, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                include_lot: true,
                include_bom: true,
                include_estimate: true,
                include_stock: true,
                include_competitor: true,
                basis: basis,
                manual_price: manualPrice > 0 ? manualPrice : null,
            }),
        });
        if (!resp.ok) { showToast('Ошибка экспорта', 'error'); return; }
        const blob = await resp.blob();
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = getFilenameFromResponse(resp) || `${configName || 'config'} SPEC-${basis.toUpperCase()}.csv`;
        a.click();
        URL.revokeObjectURL(url);
    } catch(e) {
        showToast('Ошибка экспорта', 'error');
    }
}

function escapeHtml(str) {
    if (!str) return '';
    return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function formatCurrency(val) {
    if (val == null) return '—';
    return val.toLocaleString('ru-RU', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}


</script>
{{end}}

{{template "base" .}}
