/*
  Сервис интеграции ПП Парус 8 с WEB API
  Дополнительный модуль: Sheet Parser (SHP)
*/

//------------------------------
// Подключение внешних библиотек
//------------------------------

const xml2js = require("xml2js"); //Конвертация XML в JSON и JSON в XML
const XLSX = require("./shp_utils/node_modules/xlsx"); //Парсер табличных данных

//---------------------
// Глобальные константы
//---------------------

//Количество строк, загружаемых за одно обращение к серверу
const NIMPORT_BUFF_LEN = 500;

//Форматы файлов данных */
const NSHPFL_FORMAT_XLS = 0; //MS Excel (xls, xlsx)
const NSHPFL_FORMAT_ODS = 1; //OpenDocument Spreadsheet (ods)
const NSHPFL_FORMAT_CSV = 2; //Comma-Separated Values (csv)
const NSHPFL_FORMAT_DBF = 3; //Database File (dbf)

//Кодировки файлов данных */
const NSHPFL_ENC_DEFAULT = 0; //По умолчанию
const NSHPFL_ENC_WIN1251 = 1; //Windows-1251
const NSHPFL_ENC_UTF8 = 2; //UTF-8

//------------------------
// Вспомогательные функции
//------------------------

//Разбор XML (обёртка для async/await)
const parseXML = ({ sXML, options }) => {
    //Возвращаем промис для использования async/await
    return new Promise((resolve, reject) => {
        //Разбираем XML
        xml2js.parseString(sXML, options, (err, result) => {
            //Если при разборе возникла ошибка
            if (err) reject(err);
            //Если всё успешно - возвращаем результат
            else resolve(result);
        });
    });
};

//Конвертация в XML
const toXML = ({ rootName, data, headless = false }) => {
    //Формируем XML
    const builder = new xml2js.Builder({ rootName, headless });
    return builder.buildObject(data);
};

//Получение текстового представления ячейки
const getCellText = cl => {
    //Если ячейки нет - возвращаем пустую строку
    if (!cl) return "";
    //Если есть значение v - берём его
    if (cl.v !== null && cl.v !== undefined) return String(cl.v);
    //Если есть форматированное значение w - берём его
    if (cl.w !== null && cl.w !== undefined) return String(cl.w);
    //Если значений нет - возвращаем пустую строку
    return "";
};

//Построение индекса колонок по заголовкам (имя заголовка -> номер колонки)
const buildHeadersIndex = ({ sheet, sheetData, nHeaderLineNo }) => {
    //Если листа нет - индекса заголовков нет
    if (!sheet) return null;
    //Если данных листа нет - индекса заголовков нет
    if (!Array.isArray(sheetData) || sheetData.length === 0) return null;
    //Номер строки заголовков должен быть валидным (1..)
    const nHdrLine = Number.isFinite(nHeaderLineNo) && nHeaderLineNo > 0 ? nHeaderLineNo : null;
    //Если номер строки заголовков невалиден - индекса заголовков нет
    if (!nHdrLine) return null;
    //Строка заголовков
    const headerRow = sheetData[nHdrLine - 1] || [];
    //Если строки заголовков нет - индекса заголовков нет
    if (!Array.isArray(headerRow) || headerRow.length === 0) return null;
    //Создаём резолвер объединённых ячеек
    const { resolveCell } = createMergedCellResolver(sheet);
    //Результат индекса
    const idx = new Map();
    //Обходим все колонки строки заголовков
    for (let c = 0; c < headerRow.length; c++) {
        //Номер колонки
        const colNo = c + 1;
        //Резолвим ячейку заголовка
        const cl = resolveCell(nHdrLine - 1, c, (headerRow || [])[c]);
        //Текст заголовка
        const sName = getCellText(cl).trim();
        //Пустые заголовки не индексируем
        if (!sName) continue;
        //Ключ заголовка
        const key = sName.toUpperCase();
        //Если такой заголовок уже есть - не перезаписываем
        if (idx.has(key)) continue;
        //Сохраняем номер колонки
        idx.set(key, colNo);
    }
    //Если индекс пустой - возвращаем null
    if (idx.size === 0) return null;
    //Возвращаем индекс
    return idx;
};

//Формирование XML заголовков таблицы
const buildTableHeadersXML = ({ workbook, sSheetName, aColumns, nColumnsCount, nHeaderLineNo }) => {
    //Если книги нет - заголовки построить нельзя
    if (!workbook) return null;
    //Если имя листа не задано - заголовки построить нельзя
    if (!sSheetName) return null;
    //Получаем лист
    const sheet = workbook.Sheets?.[sSheetName] || null;
    //Если листа нет - заголовки построить нельзя
    if (!sheet) return null;
    //Dense-данные листа (строки/ячейки)
    const sheetData = sheet?.["!data"] || [];
    //Если данных нет - заголовки построить нельзя
    if (!Array.isArray(sheetData) || sheetData.length === 0) return null;
    //Номер строки заголовков должен быть валидным (1..)
    const nHdrLine = Number.isFinite(nHeaderLineNo) && nHeaderLineNo > 0 ? nHeaderLineNo : 1;
    //Получаем строку заголовков
    const headerRow = sheetData[nHdrLine - 1] || [];
    //Создаём резолвер объединённых ячеек
    const { hasMerges, resolveCell } = createMergedCellResolver(sheet);
    //Нормализуем количество колонок таблицы
    const nCols = Number.isFinite(nColumnsCount) && nColumnsCount > 0 ? nColumnsCount : 0;
    //Если колонок нет - формируем пустой RHEADERS
    if (!nCols) return toXML({ rootName: "RHEADERS", data: {}, headless: true });
    //Список заголовков (RHEADER)
    const rHeaders = [];
    //Если задан список колонок
    if (Array.isArray(aColumns) && aColumns.length > 0) {
        //Обходим все выбранные колонки в заданном порядке
        for (let i = 0; i < aColumns.length; i++) {
            //Номер исходной колонки
            const colNo = aColumns[i];
            //Если номер колонки невалиден - пропускаем
            if (!Number.isFinite(colNo) || colNo < 1) continue;
            //Резолвим ячейку заголовка
            const cl = resolveCell(nHdrLine - 1, colNo - 1, (headerRow || [])[colNo - 1]);
            //Текст заголовка
            const sName = getCellText(cl).trim();
            //Добавляем RHEADER
            rHeaders.push({ NCOLUMN: colNo, SCOLUMN: sName });
        }
    }
    //Иначе - заголовки строим по фактическому набору колонок (1..nCols)
    else {
        //Определяем количество колонок в строке заголовков
        const nHeaderRowCols = hasMerges ? nCols : (headerRow || []).length;
        const nIter = Math.max(nCols, nHeaderRowCols, 0);
        //Обходим колонки 1..nIter
        for (let c = 0; c < nIter; c++) {
            //Номер колонки
            const colNo = c + 1;
            //Резолвим ячейку заголовка
            const cl = resolveCell(nHdrLine - 1, c, (headerRow || [])[c]);
            //Текст заголовка
            const sName = getCellText(cl).trim();
            //Добавляем RHEADER
            rHeaders.push({ NCOLUMN: colNo, SCOLUMN: sName });
        }
    }
    //Собираем XML заголовков
    return toXML({
        rootName: "RHEADERS",
        data: {
            ...(rHeaders.length > 0 ? { RHEADER: rHeaders } : {})
        },
        headless: true
    });
};

//Преобразование "одиночное значение | массив | undefined" -> массив
const toArray = value => {
    //Если значения нет - возвращаем пустой массив
    if (!value) return [];
    //Если уже массив - возвращаем как есть, иначе оборачиваем
    return Array.isArray(value) ? value : [value];
};

//Разбор параметров выполнения SHP (clOptions: XML -> JSON)
const parseShpOptionsXML = async sOptionsXML => {
    //Если параметр отсутствует или не строка - параметров выполнения нет
    if (!sOptionsXML || !(sOptionsXML instanceof String || typeof sOptionsXML === "string")) return null;
    //Если строка пустая - параметров выполнения нет
    if (!String(sOptionsXML).trim()) return null;
    //Результат парсинга XML
    let parseRes = null;
    try {
        //Парсим XML
        parseRes = await parseXML({
            sXML: sOptionsXML,
            options: {
                explicitArray: false,
                mergeAttrs: true,
                valueProcessors: [xml2js.processors.parseNumbers, xml2js.processors.parseBooleans]
            }
        });
    } catch (e) {
        //Ошибка разбора параметров выполнения
        throw new Error(`Ошибка разбора XML параметров выполнения: ${e.message || e}`);
    }
    //Корневой элемент параметров выполнения
    const root = parseRes?.OPTIONS || null;
    //Если корня нет - параметров выполнения нет
    if (!root) return null;
    //OPTION может быть одиночным объектом или массивом
    const options = toArray(root.OPTION).filter(Boolean);
    //Возвращаем нормализованный объект
    return { options };
};

//Преобразование номера/имени колонки в число (A->1, B->2, AA->27, ...)
const normalizeColumn = value => {
    //Если значение отсутствует - возвращаем null
    if (value === null || value === undefined) return null;
    //Приводим к строке и убираем пробелы
    const sVal = String(value).trim();
    //Пустое значение - нечего нормализовать
    if (!sVal) return null;
    //Если это число в строковом представлении
    if (/^\d+$/.test(sVal)) {
        //Преобразуем в число
        const n = parseInt(sVal, 10);
        //Колонки начинаются с 1
        return Number.isFinite(n) && n > 0 ? n : null;
    }
    //Приводим буквы к верхнему регистру
    const s = sVal.toUpperCase();
    //Ожидаем только латинские буквы (A..Z)
    if (!/^[A-Z]+$/.test(s)) return null;
    //Результат в десятичном виде
    let n = 0;
    //Переводим base-26 представление в десятичное
    for (let i = 0; i < s.length; i++) {
        //A=1 ... Z=26
        n = n * 26 + (s.charCodeAt(i) - 64);
    }
    //Возвращаем результат, если он валиден
    return n > 0 ? n : null;
};

//Преобразование имени/номера колонки в число с учётом заголовков
const normalizeColumnWithHeaders = ({ value, headersIndex }) => {
    //Если значение отсутствует - возвращаем null
    if (value === null || value === undefined) return null;
    //Приводим к строке и убираем пробелы
    const sVal = String(value).trim();
    //Пустое значение - нечего нормализовать
    if (!sVal) return null;
    //Если задан индекс заголовков - сначала ищем по нему
    if (headersIndex && headersIndex instanceof Map) {
        //Ключ поиска
        const key = sVal.toUpperCase();
        //Пробуем найти колонку по заголовку
        const nByHdr = headersIndex.get(key);
        //Если нашли валидный номер - возвращаем
        if (Number.isFinite(nByHdr) && nByHdr > 0) return nByHdr;
    }
    //Если по заголовку не нашли - преобразуем имя колонки в число
    return normalizeColumn(sVal);
};

//Формирование списка индексов (1..max) по описанию (RANGE/ENUM/COUNT)
const buildIndexList = ({ selector, nMax, normalizeItem, getDefaultFrom }) => {
    //Нормализуем максимум
    const nMaxSafe = Number.isFinite(nMax) && nMax > 0 ? nMax : 0;
    //Если селектора нет или максимум не задан - считаем что фильтра нет
    if (!selector || !nMaxSafe) return null;
    //Тип выборки (RANGE/ENUM/COUNT)
    const sType = String(selector.SADDRESS_TYPE || "")
        .trim()
        .toUpperCase();
    //Если тип не задан - считаем что фильтра нет
    if (!sType) return null;
    //Добавление уникальных значений в множество
    const pushUnique = (set, value) => {
        //Должно быть число
        if (!Number.isFinite(value)) return;
        //Должно попадать в диапазон 1..nMaxSafe
        if (value < 1 || value > nMaxSafe) return;
        //Добавляем уникально
        set.add(value);
    };
    //Множество для уникальных значений
    const res = new Set();
    //Адрес-диапазон
    if (sType === "RANGE") {
        //Узел диапазона
        const range = selector.RADDRESS_RANGE || null;
        //Начало диапазона обязательно
        const nFrom = normalizeItem?.(range?.SVALUE_FROM);
        //Если начала нет - фильтр считаем некорректным
        if (!Number.isFinite(nFrom) || nFrom < 1) return null;
        //Конец диапазона опционален
        let nTo = normalizeItem?.(range?.SVALUE_TO);
        //Если конец не указан - берём до конца
        if (!Number.isFinite(nTo) || nTo < nFrom) nTo = nMaxSafe;
        //Заполняем диапазон
        for (let i = nFrom; i <= Math.min(nTo, nMaxSafe); i++) pushUnique(res, i);
        //Возвращаем отсортированный список
        return Array.from(res).sort((a, b) => a - b);
    }
    //Адрес-перечисление
    if (sType === "ENUM") {
        //Узел перечисления
        const en = selector.RADDRESS_ENUM || null;
        //Значение может быть одиночным значением или массивом
        for (const v of toArray(en?.SVALUE)) pushUnique(res, normalizeItem?.(v));
        //Возвращаем отсортированный список
        return Array.from(res).sort((a, b) => a - b);
    }
    //Адрес-количество
    if (sType === "COUNT") {
        //Узел количества
        const cnt = selector.RADDRESS_COUNT || null;
        //Количество берём из SVALUE
        const nCount = parseInt(String(cnt?.SVALUE ?? "").trim(), 10);
        //Количество должно быть > 0
        if (!Number.isFinite(nCount) || nCount <= 0) return null;
        //Признак, что старт задан явно
        const bHasFrom = cnt?.SVALUE_FROM !== null && cnt?.SVALUE_FROM !== undefined && String(cnt?.SVALUE_FROM).trim() !== "";
        //Старт опционален (если не задан - берём вычисляемое значение, иначе 1)
        const nFromRaw = bHasFrom ? normalizeItem?.(cnt?.SVALUE_FROM) : (getDefaultFrom?.() ?? 1);
        //Если старт некорректен - начинаем с 1
        const nFromSafe = Number.isFinite(nFromRaw) && nFromRaw > 0 ? nFromRaw : 1;
        //Добавляем нужное количество индексов
        for (let i = nFromSafe; i < nFromSafe + nCount; i++) pushUnique(res, i);
        //Возвращаем отсортированный список
        return Array.from(res).sort((a, b) => a - b);
    }
    //Неизвестный тип
    return null;
};

//Формирование списка всех индексов (1..nMax)
const buildAllIndexList = ({ nMax }) => {
    //Нормализуем максимум
    const nMaxSafe = Number.isFinite(nMax) && nMax > 0 ? nMax : 0;
    //Если максимум некорректен - возвращаем пустой список
    if (!nMaxSafe) return [];
    //Создаём массив нужной длины
    const res = new Array(nMaxSafe);
    //Заполняем индексами
    for (let i = 0; i < nMaxSafe; i++) res[i] = i + 1;
    //Возвращаем результат
    return res;
};

//Проверка наличия данных в листе
const isSheetNotEmpty = ({ workbook, sSheetName }) =>
    //Лист не пустой, если есть dense-данные и они содержат хотя бы одну строку
    Array.isArray(workbook?.Sheets[sSheetName]?.["!data"]) && workbook?.Sheets[sSheetName]?.["!data"].length > 0;

//Подсчёт предельного количества столбцов в листе
const getSheetColumnsCount = ({ workbook, sSheetName }) => {
    //Получаем dense-данные листа
    const rows = workbook?.Sheets?.[sSheetName]?.["!data"];
    //Если данных нет - возвращаем 0
    if (!Array.isArray(rows) || rows.length === 0) return 0;
    //Возвращаем максимальную длину строки (количество колонок)
    return rows.reduce((cnt, curRow) => Math.max(cnt, (curRow || []).length), 0);
};

//Проверка, что ячейка содержит данные (v/w)
const isCellHasData = cl => {
    //Если ячейки нет - данных нет
    if (!cl) return false;
    //Если есть значение v - проверяем его на пустоту
    if (cl.v !== null && cl.v !== undefined && String(cl.v) !== "") return true;
    //Если есть текстовое значение w - проверяем его на пустоту
    if (cl.w !== null && cl.w !== undefined && String(cl.w) !== "") return true;
    //Данных нет
    return false;
};

//Поиск первой строки, где есть данные
const getFirstDataLineNo = ({ sheetData }) => {
    //Если данных листа нет - возвращаем 1
    if (!Array.isArray(sheetData) || sheetData.length === 0) return 1;
    //Обходим строки сверху вниз
    for (let r = 0; r < sheetData.length; r++) {
        //Строка листа (массив ячеек)
        const row = sheetData[r] || [];
        //Обходим ячейки строки
        for (let c = 0; c < row.length; c++) {
            //Если нашли данные - возвращаем номер строки
            if (isCellHasData(row[c])) return r + 1;
        }
    }
    //Если данных не нашли - возвращаем 1
    return 1;
};

//Поиск первой колонки, где есть данные
const getFirstDataColumnNo = ({ sheetData }) => {
    //Если данных листа нет - возвращаем 1
    if (!Array.isArray(sheetData) || sheetData.length === 0) return 1;
    //Минимальный индекс колонки с данными
    let minCol = null;
    //Обходим строки
    for (let r = 0; r < sheetData.length; r++) {
        //Строка листа (массив ячеек)
        const row = sheetData[r] || [];
        //Обходим ячейки строки
        for (let c = 0; c < row.length; c++) {
            //Если данных нет - пропускаем
            if (!isCellHasData(row[c])) continue;
            //Фиксируем минимальную колонку с данными
            minCol = minCol === null ? c : Math.min(minCol, c);
        }
    }
    //Если данных не нашли - возвращаем 1
    if (minCol === null) return 1;
    //Возвращаем номер колонки
    return minCol + 1;
};

//Построение таблиц для импорта по параметрам выполнения
const buildDataTablesByOptions = ({ workbook, nShpFileRn, options, sDataSheetName, bIgnoreSheetName, bHasHeaders }) => {
    //Результирующий список таблиц
    const res = [];
    //Обходим все OPTION
    for (const opt of toArray(options)) {
        //Имя листа, к которому относится параметр
        const sSheetName = String(opt?.SSHEET || "").trim();
        //Если имя листа не задано - пропускаем
        if (!sSheetName) continue;
        //Если лист пустой - пропускаем
        if (!isSheetNotEmpty({ workbook, sSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName })) continue;
        //Список таблиц для данного параметра
        const rTables = toArray(opt?.RTABLES?.RTABLE);
        //Если таблицы не указаны - пропускаем
        if (rTables.length === 0) continue;
        //Фактическое количество колонок листа
        const nColumnsCountRaw = getSheetColumnsCount({ workbook, sSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName });
        //Фактическое количество строк листа
        const nLinesCountRaw = (workbook.Sheets[bIgnoreSheetName ? sDataSheetName : sSheetName]["!data"] || []).length;
        //Обходим описанные таблицы
        for (const rt of rTables) {
            //Имя таблицы
            const sTableName = rt?.SNAME ? String(rt.SNAME).trim() : null;
            //Лист, откуда берём данные
            const sheet = workbook?.Sheets?.[bIgnoreSheetName ? sDataSheetName : sSheetName] || null;
            //Dense-данные листа
            const sheetData = sheet?.["!data"] || [];
            //Номер строки заголовков
            const nHeaderLineNo = bHasHeaders ? getFirstDataLineNo({ sheetData }) : null;
            //Индекс заголовков
            const headersIndex = bHasHeaders ? buildHeadersIndex({ sheet, sheetData, nHeaderLineNo }) : null;
            //Формируем список колонок по описателю
            const aCols = buildIndexList({
                selector: rt?.RCOLUMNS || null,
                nMax: nColumnsCountRaw,
                normalizeItem: v => normalizeColumnWithHeaders({ value: v, headersIndex }),
                getDefaultFrom: () => getFirstDataColumnNo({ sheetData })
            });
            //Формируем список строк по описателю
            const aLines = buildIndexList({
                selector: rt?.RLINES || null,
                nMax: nLinesCountRaw,
                normalizeItem: v => {
                    //Если значения нет - не нормализуем
                    if (v === null || v === undefined) return null;
                    //Парсим строку как число
                    const n = parseInt(String(v).trim(), 10);
                    //Индекс строки должен быть положительным
                    return Number.isFinite(n) && n > 0 ? n : null;
                },
                getDefaultFrom: () => getFirstDataLineNo({ sheetData })
            });
            //Добавляем описание таблицы в результат
            res.push({
                sSheetName,
                sDataSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName,
                sTableName,
                nShpFileRn,
                aColumns: aCols,
                aLines
            });
        }
    }
    //Возвращаем список таблиц
    return res;
};

//Преобразование значения ячейки в формат импорта
const makeImportCell = ({ cl, n }) => {
    //Нормализуем номер колонки (1..)
    const nCol = Number.isFinite(n) && n > 0 ? n : null;
    //Если ячейки нет - пишем пустую ячейку
    if (!cl) return { $: { t: "z", n: nCol }, v: null };
    //Тип ячейки
    const t = cl.t || "z";
    //Значение ячейки (даты приводим к ISO-строке)
    const v = (t === "d" ? cl.v?.toISOString?.() : cl.v) || cl.w || null;
    //Возвращаем формат ячейки для импорта
    return { $: { t, n: nCol }, v };
};

//Резолвер значений для объединённых ячеек
const createMergedCellResolver = sheet => {
    //Считываем описания объединений ячеек (если они есть)
    const merges = Array.isArray(sheet?.["!merges"]) ? sheet["!merges"] : [];
    //Если объединений нет - возвращаем быстрый резолвер без дополнительной логики
    if (merges.length === 0) {
        return {
            hasMerges: false,
            resolveCell: (_r, _c, cell) => cell || null
        };
    }
    //Создаём индекс объединений по строкам
    const byRow = new Map();
    //Обходим все описания объединений
    for (const m of merges) {
        //Начальная координата диапазона объединения
        const s = m?.s;
        //Конечная координата диапазона объединения
        const e = m?.e;
        //Если координат нет - пропускаем элемент
        if (!s || !e) continue;
        //Нормализуем начало по строкам
        const rFrom = Math.min(s.r ?? 0, e.r ?? 0);
        //Нормализуем конец по строкам
        const rTo = Math.max(s.r ?? 0, e.r ?? 0);
        //Нормализуем начало по колонкам
        const cFrom = Math.min(s.c ?? 0, e.c ?? 0);
        //Нормализуем конец по колонкам
        const cTo = Math.max(s.c ?? 0, e.c ?? 0);
        //Проверяем валидность нормализованных координат
        const bOk = Number.isFinite(rFrom) && Number.isFinite(rTo) && Number.isFinite(cFrom) && Number.isFinite(cTo);
        //Если координаты невалидны - пропускаем объединение
        if (!bOk) continue;
        //Индексируем объединение по всем строкам диапазона
        for (let r = rFrom; r <= rTo; r++) {
            //Текущий список объединений по строке (если уже есть)
            const arr = byRow.get(r);
            //Сохраняем диапазон и “ведущую” ячейку
            const item = { rFrom, rTo, cFrom, cTo, rLead: rFrom, cLead: cFrom };
            //Если массив по строке уже существует - добавляем
            if (arr) arr.push(item);
            //Иначе создаём новый список по строке
            else byRow.set(r, [item]);
        }
    }
    //Получаем dense-данные листа (для доступа к “ведущей” ячейке)
    const data = sheet?.["!data"] || [];
    //Возвращаем резолвер с поддержкой объединений
    return {
        hasMerges: true,
        resolveCell: (r, c, cell) => {
            //Если ячейка уже есть - возвращаем её без изменений
            if (cell) return cell;
            //Получаем список объединений для текущей строки
            const rowMerges = byRow.get(r);
            //Если объединений в этой строке нет - возвращаем null
            if (!rowMerges || rowMerges.length === 0) return null;
            //Проверяем попадание колонки в один из диапазонов объединений
            for (const m of rowMerges) {
                //Если колонка вне диапазона - проверяем следующее объединение
                if (c < m.cFrom || c > m.cTo) continue;
                //Возвращаем “ведущую” ячейку диапазона
                return (data[m.rLead] || [])[m.cLead] || null;
            }
            //Если не попали ни в одно объединение - возвращаем null
            return null;
        }
    };
};

//------------
// Тело модуля
//------------

//Обработчик "До"
const importBefore = async ({ queue, dbConn }) => {
    //Если задано тело сообщения
    if (queue.blMsg) {
        //В теле должен быть рег. номер файла данных (EXSEXTSHPFL)
        const nShpFileRn = parseInt(queue.blMsg.toString(), 10);
        //Если там рег. номер (число)
        if (!isNaN(nShpFileRn)) {
            //Читаем по нему запись файла данных
            const shpFileData = await dbConn.executeStored({
                connection: dbConn.connection,
                sName: "PKG_EXS_EXT_SHP.FILE_GET",
                inPrms: { NEXSEXTSHPFL: nShpFileRn },
                outPrms: { RFILE: dbConn.connector.DT_CURSOR }
            });
            //Если запись найдена
            if (shpFileData.RFILE.length === 1) {
                //Разбираем её
                try {
                    //Признак наличия заголовков
                    const bHasHeaders = String(shpFileData.RFILE[0]?.nHasHeaders ?? "0").trim() === "1";
                    //Формат файла данных
                    const nFileFormat = shpFileData.RFILE[0]?.nFormat;
                    //Заголовки формируем только для CSV/DBF и только если они есть
                    const bNeedSendHeaders = bHasHeaders && [NSHPFL_FORMAT_CSV, NSHPFL_FORMAT_DBF].includes(nFileFormat);
                    //Объект рабочей книги
                    let workbook = null;
                    try {
                        //Кодировка файла данных
                        const nEnc = shpFileData.RFILE[0].nEnc;
                        //Кодовая страница (параметры чтения XLSX)
                        const cp = {};
                        //Байты файла данных
                        let blData = shpFileData.RFILE[0].blFileData;
                        //Если это CSV
                        if (nFileFormat === NSHPFL_FORMAT_CSV) {
                            //Если указана Win-1251 - перекодируем CSV в UTF-8
                            if (nEnc === NSHPFL_ENC_WIN1251) {
                                //Декодируем исходные байты как windows-1251
                                const s = new TextDecoder("windows-1251").decode(blData);
                                //Преобразуем строку обратно в байты UTF-8
                                blData = Buffer.from(s);
                            }
                            //Задаём codepage UTF-8
                            cp.codepage = 65001;
                        }
                        //Если это DBF
                        else if (nFileFormat === NSHPFL_FORMAT_DBF) {
                            //Если указана Win-1251
                            if (nEnc === NSHPFL_ENC_WIN1251)
                                //Задаём codepage 1251
                                cp.codepage = 1251;
                            else
                                //Задаём codepage UTF-8
                                cp.codepage = 65001;
                        }
                        //Выполняем парсинг
                        workbook = XLSX.read(blData, { dense: true, cellDates: true, ...cp });
                    } catch (e) {
                        //Ошибка чтения/парсинга файла данных
                        throw new Error(`Ошибка разбора файла данных: ${e.message}`);
                    }
                    //Если есть сведения о листахх
                    if (workbook && workbook?.SheetNames?.length > 0) {
                        //Признак формата, где листов фактически нет (csv/dbf) или их не нужно учитывать
                        const bIgnoreSheetName = ![NSHPFL_FORMAT_XLS, NSHPFL_FORMAT_ODS].includes(nFileFormat);
                        //Имя листа с данными (для csv/dbf используем первый лист книги)
                        const sDataSheetName = workbook.SheetNames[0];
                        //Разбор параметров выполнения (если есть)
                        const shpOptions = await parseShpOptionsXML(shpFileData.RFILE[0]?.clOptions);
                        //Определим буфер для хранения таблиц файла данных
                        const dataTables = [];
                        //Формирование списка таблиц для импорта
                        const tablesSpec = shpOptions
                            ? buildDataTablesByOptions({
                                  workbook,
                                  nShpFileRn,
                                  options: shpOptions.options,
                                  sDataSheetName,
                                  bIgnoreSheetName,
                                  bHasHeaders
                              })
                            : (bIgnoreSheetName
                                  ? [sDataSheetName]
                                  : workbook.SheetNames.filter(sSheetName => isSheetNotEmpty({ workbook, sSheetName }))
                              ).map(sSheetName => ({
                                  sSheetName,
                                  sDataSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName,
                                  sTableName: null,
                                  nShpFileRn,
                                  aColumns: null,
                                  aLines: null
                              }));
                        //Регистрируем таблицы на сервере
                        for (const tbl of tablesSpec) {
                            //Считаем фактическое количество колонок листа
                            const nColumnsCountRaw = getSheetColumnsCount({ workbook, sSheetName: tbl.sDataSheetName || tbl.sSheetName });
                            //Считаем фактическое количество строк листа
                            const nLinesCountRaw = (workbook.Sheets[tbl.sDataSheetName || tbl.sSheetName]["!data"] || []).length;
                            //Список колонок (или null - значит все)
                            const aColumns = Array.isArray(tbl.aColumns) ? tbl.aColumns : null;
                            //Список строк (или null - значит все)
                            const aLines = Array.isArray(tbl.aLines) ? tbl.aLines : null;
                            //Количество колонок для описания таблицы
                            const nColumnsCount = aColumns ? aColumns.length : nColumnsCountRaw;
                            //Определяем лист, с которого читаем данные
                            const sDataSheetName = tbl.sDataSheetName || tbl.sSheetName;
                            //Dense-данные листа
                            const sheetData = workbook?.Sheets?.[sDataSheetName]?.["!data"] || [];
                            //Номер строки заголовков (если заголовки нужны - берём первую строку с данными, иначе null)
                            const nHeaderLineNo = bNeedSendHeaders ? getFirstDataLineNo({ sheetData }) : null;
                            //Количество строк для описания таблицы
                            const nLinesCount = (() => {
                                //Если заголовков нет - количество строк
                                const nRaw = aLines ? aLines.length : nLinesCountRaw;
                                //Если заголовки не требуются - возвращаем как есть
                                if (!bNeedSendHeaders) return nRaw;
                                //Если строк нет - нечего исключать
                                if (!Number.isFinite(nRaw) || nRaw <= 0) return nRaw;
                                //Если номер заголовка невалиден
                                if (!Number.isFinite(nHeaderLineNo) || nHeaderLineNo <= 0) return nRaw;
                                //Если выбран конкретный список строк - исключаем заголовок только если он туда попал
                                if (aLines) return aLines.includes(nHeaderLineNo) ? Math.max(nRaw - 1, 0) : nRaw;
                                //Если строки не фильтруются - заголовок считаем частью данных листа и исключаем его
                                return nHeaderLineNo <= nLinesCountRaw ? Math.max(nRaw - 1, 0) : nRaw;
                            })();
                            //Формируем XML заголовков (только если требуется отправка заголовков)
                            const sHeadersXML = bNeedSendHeaders
                                ? buildTableHeadersXML({
                                      workbook,
                                      sSheetName: sDataSheetName,
                                      aColumns,
                                      nColumnsCount,
                                      nHeaderLineNo
                                  })
                                : null;
                            try {
                                //Создаём таблицу в БД
                                const shpFileDataTable = await dbConn.executeStored({
                                    connection: dbConn.connection,
                                    sName: "PKG_EXS_EXT_SHP.FILE_TABLE_ADD",
                                    inPrms: {
                                        NEXSEXTSHPFL: nShpFileRn,
                                        SSHEET: tbl.sSheetName,
                                        STBL: tbl.sTableName,
                                        NCOLS: nColumnsCount,
                                        ...(sHeadersXML ? { CHEADERS: sHeadersXML } : {}),
                                        NLINES: nLinesCount
                                    },
                                    outPrms: { NEXSEXTSHPFLTB: dbConn.connector.DT_NUMBER }
                                });
                                //Сохраняем данные таблицы для дальнейшей загрузки строк
                                dataTables.push({
                                    sSheetName: tbl.sSheetName,
                                    sDataSheetName: tbl.sDataSheetName || tbl.sSheetName,
                                    sTableName: tbl.sTableName,
                                    nTableRn: shpFileDataTable.NEXSEXTSHPFLTB,
                                    nColumnsCount,
                                    nLinesCount,
                                    aColumns,
                                    aLines,
                                    nHeaderLineNo
                                });
                            } catch (e) {
                                //Ошибка регистрации таблицы
                                throw new Error(
                                    `Ошибка сохранения описания таблицы "${tbl.sTableName || "<НЕ УКАЗАНА>"}" листа "${tbl.sSheetName || "<НЕ УКАЗАН>"}": ${e.message}`
                                );
                            }
                        }
                        //Теперь загружаем таблицы - обходим их последовательно
                        for (const dataTable of dataTables) {
                            //Буфер строк для порционной загрузки
                            let impBuff = [];
                            //Сброс буфера
                            const flushBuff = async () => {
                                //Если буфер пуст - нечего сохранять
                                if (impBuff.length === 0) return;
                                //Преобразуем буфер в XML
                                let sXML = null;
                                try {
                                    //Сборка XML для порции строк
                                    sXML = toXML({ rootName: "lines", data: impBuff });
                                } catch (e) {
                                    //Ошибка сборки XML
                                    throw new Error(`Ошибка преобразования буфера в XML: ${e.message}.`);
                                }
                                //Сохраняем на сервере
                                const nLineFrom = impBuff[0].line.$.n;
                                const nLineTo = impBuff[impBuff.length - 1].line.$.n;
                                try {
                                    //Сохраняем порцию строк в БД
                                    await dbConn.executeStored({
                                        connection: dbConn.connection,
                                        sName: "PKG_EXS_EXT_SHP.FILE_TABLE_LINE_ADD",
                                        inPrms: {
                                            NEXSEXTSHPFLTB: dataTable.nTableRn,
                                            NLN_FROM: nLineFrom,
                                            NLN_TO: nLineTo,
                                            BDT: Buffer.from(sXML)
                                        }
                                    });
                                } catch (e) {
                                    //Ошибка сохранения порции строк
                                    throw new Error(
                                        `Ошибка сохранения строк (c ${nLineFrom} - по ${nLineTo}) таблицы "${dataTable.sTableName || "<НЕ УКАЗАНА>"}" листа "${dataTable.sSheetName || "<НЕ УКАЗАН>"}": ${e.message}`
                                    );
                                }
                                //Очищаем буфер после успешной отправки
                                impBuff = [];
                            };
                            //Получаем объект листа
                            const sheet = workbook.Sheets[dataTable.sDataSheetName || dataTable.sSheetName];
                            //Получаем dense-данные листа (строки/ячейки)
                            const sheetData = sheet?.["!data"] || [];
                            //Создаём резолвер объединённых ячеек
                            const { hasMerges, resolveCell } = createMergedCellResolver(sheet);
                            //Формируем список строк (или берём все)
                            const aLines = dataTable.aLines ? dataTable.aLines : buildAllIndexList({ nMax: sheetData.length });
                            //Список колонок (или null - значит все)
                            const aColumns = dataTable.aColumns;
                            //Предыдущая строка для контроля непрерывности (NLN_FROM/NLN_TO)
                            let prevLineNo = null;
                            for (const lineNo of aLines) {
                                //Защита от невалидных индексов
                                if (!Number.isFinite(lineNo) || lineNo < 1 || lineNo > sheetData.length) continue;
                                //Если это строка заголовков - не отправляем её как данные
                                if (Number.isFinite(dataTable.nHeaderLineNo) && lineNo === dataTable.nHeaderLineNo) continue;
                                //Если есть разрыв диапазона строк - сбрасываем буфер (важно для NLN_FROM/NLN_TO)
                                if (prevLineNo !== null && lineNo !== prevLineNo + 1) await flushBuff();
                                //Получаем строку листа
                                const sheetLine = sheetData[lineNo - 1] || [];
                                //Формируем ячейки строки с учётом фильтра колонок и объединений
                                const cells = aColumns
                                    ? aColumns.map(colNo =>
                                          //Для выбранной колонки получаем ячейку (с учётом объединения) и приводим к формату импорта
                                          makeImportCell({
                                              //Резолвим ячейку по исходной колонке
                                              cl: resolveCell(lineNo - 1, colNo - 1, (sheetLine || [])[colNo - 1]),
                                              //Пишем номер исходной колонки
                                              n: colNo
                                          })
                                      )
                                    : (() => {
                                          //Определяем количество колонок для строки (в режиме объединений берём из описания таблицы)
                                          const nCols = hasMerges ? dataTable.nColumnsCount : (sheetLine || []).length;
                                          //Готовим массив ячеек нужной длины
                                          const res = new Array(nCols);
                                          //Обходим все колонки строки
                                          for (let c = 0; c < nCols; c++) {
                                              //Резолвим ячейку (с учётом объединения) и приводим к формату импорта
                                              res[c] = makeImportCell({
                                                  //Резолвим ячейку по исходной колонке
                                                  cl: resolveCell(lineNo - 1, c, (sheetLine || [])[c]),
                                                  //Пишем номер исходной колонки
                                                  n: c + 1
                                              });
                                          }
                                          //Возвращаем массив ячеек строки
                                          return res;
                                      })();
                                //Добавляем строку в буфер импорта
                                impBuff.push({
                                    line: {
                                        $: { n: lineNo },
                                        cell: cells
                                    }
                                });
                                //Запоминаем предыдущую строку
                                prevLineNo = lineNo;
                                //Если набрали порцию - отправляем
                                if (impBuff.length === NIMPORT_BUFF_LEN) await flushBuff();
                            }
                            //Сохраняем остаток буфера
                            await flushBuff();
                        }
                    }
                } catch (e) {
                    //Любая ошибка импорта
                    throw new Error(e.message || "Неожиданная ошибка.");
                }
            }
            //Не нашли запись файла данных
            else throw new Error("Файл данных не определен.");
        }
        //Не смогли считать рег. номер из тела сообщения
        else throw new Error("Некорректный идентификатор файла данных для разбора.");
    }
    //Тело сообщения не задано
    else throw new Error("Нет данных для разбора.");
    //Обработку закончили - продолжать не надо, мы всё сделали в обработчике
    return { bStopPropagation: true };
};

//-----------------
// Интерфейс модуля
//-----------------

exports.importBefore = importBefore;
