/**
 * Telegram-бот для внутренних нужд компании.
 * Версия: 2.4 (Улучшенная отмена действий и навигация)
 * Дата обновления: 15.07.2025
 * 
 * Изменения v2.4:
 * - ДОБАВЛЕНО: Кнопка "Назад" на экранах запроса ввода (ID терминала, значение для QR), позволяющая отменить действие.
 * - ИСПРАВЛЕНО: При отмене действия состояние пользователя корректно очищается.
 * - ИЗМЕНЕНО: На экранах результатов кнопки переупорядочены для лучшего UX ("Назад" сверху, "Повторить" снизу).
 */

// --- 1. ЦЕНТРАЛИЗОВАННАЯ КОНФИГУРАЦИЯ ---
var CONSTANTS = {
  PROPERTIES: {
    BOT_TOKEN: PropertiesService.getScriptProperties().getProperty('BOT_TOKEN'),
    GROUP_ID: PropertiesService.getScriptProperties().getProperty('GROUP_ID'),
    PAYSLIP_FOLDER_ID: PropertiesService.getScriptProperties().getProperty('PAYSLIP_FOLDER_ID'),
    TELEGRAM_CACHE_CHANNEL_ID: PropertiesService.getScriptProperties().getProperty('TELEGRAM_CACHE_CHANNEL_ID')
  },
  SHEETS: {
    PAYSLIP_USERS: "payslip",
    LOGS: "Bot_Logs",
    PAYSLIP_CACHE: "payslip_cache",
    INFO: "info"
  },
  CONFIG: {
    GRAPH_LIMIT_PER_DAY: 10,
    START_COOLDOWN_MINUTES: 1,
    QR_MAX_LENGTH: 10,
    ENABLE_LOGGING: 0,
    CACHE_EXPIRATION_SECONDS: 600
  },
  KEYS: {
    USER_STATE_PREFIX: 'state_',
    USER_GRAPH_DATA_PREFIX: 'graph_data_',
    CACHE_INFO_DATA: 'info_data',
    CACHE_PAYSLIP_USERS: 'payslip_users'
  }
};

// --- 2. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ И СЕРВИСЫ ---
var telegramUrl = "https://api.telegram.org/bot" + CONSTANTS.PROPERTIES.BOT_TOKEN;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var userProperties = PropertiesService.getUserProperties();
var scriptCache = CacheService.getScriptCache();

// --- 3. ГЛАВНАЯ ФУНКЦИЯ (ТОЧКА ВХОДА) ---
function doPost(e) {
  try {
    var contents = JSON.parse(e.postData.contents);
    var chat_id, user_id, message_id, chat_type, text, data, callback_query_id;

    if (contents.message) {
      var message = contents.message;
      chat_id = message.chat.id;
      user_id = message.from.id;
      message_id = message.message_id;
      chat_type = message.chat.type;
      text = message.text;
    } else if (contents.callback_query) {
      var callback = contents.callback_query;
      chat_id = callback.message.chat.id;
      user_id = callback.from.id;
      message_id = callback.message.message_id;
      chat_type = callback.message.chat.type;
      data = callback.data;
      callback_query_id = callback.id;
    } else {
      return;
    }

    logToSheet("doPost", `Incoming: chat_id=${chat_id}, user_id=${user_id}, type=${chat_type}, data=${text || data}, msg_id=${message_id}`); 

    if (chat_type !== "private") {
      if (text === "/start" || data) { 
        sendMessage(chat_id, "Извините, для получения конфиденциальной информации, пожалуйста, используйте бота в личном чате.");
      }
      return; 
    }
    
    if (!isUserInGroup(user_id)) {
        sendMessage(chat_id, "Доступ запрещен. Вы не являетесь участником рабочей группы.");
        return;
    }

    if (data) {
      answerCallbackQuery(callback_query_id);
      handleCallback(chat_id, user_id, data, message_id);
      return;
    }
    
    var userState = getUserState(user_id);
    if (userState && text) {
        handleStatefulInput(chat_id, user_id, text, message_id, userState);
        return;
    }
    
    if (text) {
      handleTextMessage(chat_id, user_id, text, message_id);
    }
  } catch (err) {
    logToSheet("doPost_Catch_Error", `Raw Event: ${e.postData.contents} | Error: ${err.message} | Stack: ${err.stack}`);
  }
}

// --- 4. МАРШРУТИЗАТОРЫ ЗАПРОСОВ ---
function handleCallback(chat_id, user_id, data, message_id) {
  if (data === "back_main") {
    // ИЗМЕНЕНО: При возврате в меню также очищаем состояние пользователя.
    clearUserState(user_id);
    deleteMessage(chat_id, message_id);
    sendMainMenu(chat_id, user_id);
  }
  else if (data === "main_schedule") sendScheduleMenu(chat_id, user_id, message_id);
  else if (data === "main_payslip") sendPayslipMenu(chat_id, user_id, message_id);
  else if (data === "main_info") handleInfoRequest(chat_id, user_id, message_id);
  else if (data === "main_qr") handleQrRequest(chat_id, user_id, message_id);
  else if (data.startsWith("sheet_")) sendSchedule(chat_id, user_id, data.substring(6), message_id); 
  else if (data.startsWith("payslip_")) sendPayslip(chat_id, user_id, data.substring(8), message_id);
}

function handleStatefulInput(chat_id, user_id, text, message_id, userState) {
    switch (userState.action) {
        case 'waiting_for_info_id':
            handleInfoIdInput(chat_id, user_id, text, message_id, userState.prompt_message_id);
            break;
        case 'waiting_for_qr_value':
            handleQrValueInput(chat_id, user_id, text, message_id, userState.prompt_message_id);
            break;
    }
}

function handleTextMessage(chat_id, user_id, text, message_id) {
    if (text === "/start") {
        handleStart(chat_id, user_id, message_id);
    }
}

// --- 5. ОБРАБОТЧИКИ КОМАНД (БИЗНЕС-ЛОГИКА) ---

function handleStart(chat_id, user_id, message_id) {
  deleteMessage(chat_id, message_id);
  Utilities.sleep(500);

  var lastStartKey = 'last_start_' + user_id;
  var lastStart = userProperties.getProperty(lastStartKey);
  var now = new Date().getTime();
  
  if (lastStart && (now - parseInt(lastStart)) < CONSTANTS.CONFIG.START_COOLDOWN_MINUTES * 60 * 1000) {
    sendMessage(chat_id, "Пожалуйста, подождите немного перед повторным использованием команды /start.");
    return;
  }
  userProperties.setProperty(lastStartKey, String(now)); 
  
  sendMainMenu(chat_id, user_id);
}

function sendMainMenu(chat_id, user_id, message_id = null) {
  var keyboard = [
    [{ text: "🗓️ График работы", callback_data: "main_schedule" }],
    [{ text: "📄 Расчетные листки", callback_data: "main_payslip" }],
    [{ text: "ℹ️ Информация о точке", callback_data: "main_info" }],
    [{ text: "🔳 Сгенерировать QR", callback_data: "main_qr" }]
  ];
  var replyMarkup = { inline_keyboard: keyboard };
  var text = "Выберите, что вы хотите получить:";
  
  if (message_id) {
    editMessage(chat_id, message_id, text, replyMarkup);
  } else {
    sendMessage(chat_id, text, replyMarkup);
  }
}

function sendScheduleMenu(chat_id, user_id, message_id = null) {
  var sheets = ss.getSheets();
  var keyboard = [];
  var excludedSheets = Object.values(CONSTANTS.SHEETS).map(name => name.toLowerCase()); 
  keyboard.push([{ text: "⬅️ Назад", callback_data: "back_main" }]);
  
  var currentKeyboardRow = []; 
  sheets.forEach(function(sheet) {
    var sheetName = sheet.getName();
    if (excludedSheets.includes(sheetName.toLowerCase())) return; 
    
    currentKeyboardRow.push({ text: sheetName, callback_data: "sheet_" + sheetName });
    if (currentKeyboardRow.length === 2) { 
      keyboard.push(currentKeyboardRow);
      currentKeyboardRow = [];
    }
  });
  if (currentKeyboardRow.length > 0) keyboard.push(currentKeyboardRow);
  
  var text = keyboard.length > 1 ? "Выберите месяц, чтобы получить ваш график работы:" : "Нет доступных графиков для отображения.";
  var replyMarkup = { inline_keyboard: keyboard };
  
  if (message_id) {
    editMessage(chat_id, message_id, text, replyMarkup);
  } else {
    sendMessage(chat_id, text, replyMarkup);
  }
}

function sendSchedule(chat_id, user_id, sheetName, message_id) {
  var today = new Date().toISOString().slice(0, 10); 
  var userDataKey = CONSTANTS.KEYS.USER_GRAPH_DATA_PREFIX + user_id;
  var userData = JSON.parse(userProperties.getProperty(userDataKey) || '{}');
  
  userData.count = (userData.date !== today) ? 1 : (userData.count || 0) + 1;
  userData.date = today;
  userProperties.setProperty(userDataKey, JSON.stringify(userData));
  
  if (userData.count > CONSTANTS.CONFIG.GRAPH_LIMIT_PER_DAY) {
    editMessage(chat_id, message_id, "Вы достигли дневного лимита на запросы графиков. Попробуйте снова завтра.", null);
    return;
  }
  
  var sheet = ss.getSheetByName(sheetName);
  if (!sheet) { editMessage(chat_id, message_id, "Ошибка: лист '" + sheetName + "' не найден.", null); return; }
  
  var data = sheet.getDataRange().getValues();
  if (data.length < 2) { editMessage(chat_id, message_id, "Ошибка: на листе '" + sheetName + "' нет данных.", null); return; }
  
  var headers = data[0];
  var telegramIdColumnIndex = headers.findIndex(h => String(h).trim().toLowerCase() === "telegram id");
  if (telegramIdColumnIndex === -1) { editMessage(chat_id, message_id, "Критическая ошибка в таблице: не найден столбец 'Telegram ID'.", null); return; }
  
  var userRow = data.find(row => String(row[telegramIdColumnIndex]).trim() == String(user_id));
  if (!userRow) { editMessage(chat_id, message_id, "Ваш график на листе '" + sheetName + "' не найден. Убедитесь, что ваш Telegram ID добавлен в таблицу.", null); return; }

  var parts = sheetName.split(" ");
  var monthName = parts[0];
  var year = parseInt(parts[1], 10);
  var monthIndex = getMonthNumber(monthName); 
  if (monthIndex === -1 || isNaN(year)) { editMessage(chat_id, message_id, "Не удалось определить месяц из названия листа: '" + sheetName + "'.", null); return; }
  
  var firstDayOfMonth = new Date(year, monthIndex, 1);
  var startingDayOfWeek = (firstDayOfMonth.getDay() + 6) % 7; 
  var daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
  var cellWidth = 7;
  var scheduleText = "🗓️ *Ваш график на " + sheetName + "*\n\n" + "```\n" + "Пн".padEnd(cellWidth) + "Вт".padEnd(cellWidth) + "Ср".padEnd(cellWidth) + "Чт".padEnd(cellWidth) + "Пт".padEnd(cellWidth) + "Сб".padEnd(cellWidth) + "Вс".padEnd(cellWidth) + "\n" + "-".repeat(cellWidth * 7) + "\n"; 
  var weeks = [];
  var currentWeek = { days: "", schedules: "" };
  var dayOfWeek = 0;
  
  for (var i = 0; i < startingDayOfWeek; i++) { currentWeek.days += " ".repeat(cellWidth); currentWeek.schedules += " ".repeat(cellWidth); dayOfWeek++; }
  
  for (var day = 1; day <= daysInMonth; day++) {
    var dayStr = String(day);
    var columnIndex = headers.findIndex(h => String(h).trim() === dayStr);
    var scheduleData = (columnIndex !== -1 && userRow[columnIndex]) ? String(userRow[columnIndex]).trim() : "В";
    if (scheduleData === "") scheduleData = "В";
    if (scheduleData.length >= cellWidth) scheduleData = scheduleData.substring(0, cellWidth - 1);
    
    currentWeek.days += dayStr.padEnd(cellWidth, ' ');
    currentWeek.schedules += scheduleData.padEnd(cellWidth, ' ');
    dayOfWeek++;
    
    if (dayOfWeek === 7) { weeks.push(currentWeek); currentWeek = { days: "", schedules: "" }; dayOfWeek = 0; }
  }
  
  if (dayOfWeek !== 0) { 
      while (dayOfWeek < 7) {
          currentWeek.days += " ".repeat(cellWidth);
          currentWeek.schedules += " ".repeat(cellWidth);
          dayOfWeek++;
      }
      weeks.push(currentWeek);
  }
  
  weeks.forEach(function(week) { scheduleText += week.days.trimEnd() + "\n" + week.schedules.trimEnd() + "\n\n"; });
  scheduleText = scheduleText.trimEnd() + "\n```";
  
  editMessage(chat_id, message_id, scheduleText, null, "MarkdownV2"); 
}

function sendPayslipMenu(chat_id, user_id, message_id = null) {
  var payslipCacheData = getPayslipCacheData(); 
  var availablePayslipMonths = [...new Set(payslipCacheData.map(row => row.monthYear))];
  
  availablePayslipMonths.sort((a, b) => {
    var partsA = a.split(' '); var partsB = b.split(' ');
    var dateA = new Date(parseInt(partsA[1], 10), getMonthNumber(partsA[0]), 1);
    var dateB = new Date(parseInt(partsB[1], 10), getMonthNumber(partsB[0]), 1);
    return dateA.getTime() - dateB.getTime();
  });

  var keyboard = [[{ text: "⬅️ Назад", callback_data: "back_main" }]];
  var currentKeyboardRow = [];
  availablePayslipMonths.forEach(function(monthYear) {
    currentKeyboardRow.push({ text: monthYear, callback_data: "payslip_" + monthYear.replace(' ', '_') });
    if (currentKeyboardRow.length === 2) {
      keyboard.push(currentKeyboardRow);
      currentKeyboardRow = [];
    }
  });
  if (currentKeyboardRow.length > 0) keyboard.push(currentKeyboardRow);
  
  var text = keyboard.length > 1 ? "Выберите месяц расчетного листка:" : "В папке с расчетными листками не найдено файлов или они еще не проиндексированы.";
  var replyMarkup = { inline_keyboard: keyboard };
  
  if (message_id) {
    editMessage(chat_id, message_id, text, replyMarkup);
  } else {
    sendMessage(chat_id, text, replyMarkup);
  }
}

function sendPayslip(chat_id, user_id, monthYearString, message_id) { 
  editMessage(chat_id, message_id, "📄 Ищу ваш расчетный листок...", null);
  
  var parts = monthYearString.split('_'); 
  if (parts.length < 2 || isNaN(parseInt(parts[1], 10))) {
      editMessage(chat_id, message_id, "Ошибка: Не удалось определить год из выбранного месяца.", null);
      return;
  }
  var monthName = parts[0];
  var year = parseInt(parts[1], 10);
  var monthNum = getMonthNumber(monthName) + 1; 
  var formattedMonth = monthNum < 10 ? '0' + monthNum : String(monthNum); 
  
  var userTabelNumbersMap = getPayslipUsers();
  var userTabelNumbers = userTabelNumbersMap[String(user_id)];
  if (!userTabelNumbers || userTabelNumbers.length === 0) {
    editMessage(chat_id, message_id, "Ваш Telegram ID не найден в списке для получения расчетных листков.", null);
    return;
  }
  
  var payslipCacheData = getPayslipCacheData();
  var sentPayslipsCount = 0;
  var notFound = false;

  userTabelNumbers.forEach(function(tabelNo) {
    var expectedDriveFileName = `${year}-${formattedMonth}-${tabelNo}.png`;
    var cacheEntry = payslipCacheData.find(entry => entry.driveFileName === expectedDriveFileName);
    
    if (cacheEntry && cacheEntry.telegramFileId) {
      try { 
        sendPhoto(chat_id, cacheEntry.telegramFileId, `Ваш расчетный листок за ${monthName} ${year} (Таб. №: ${tabelNo}).`); 
        sentPayslipsCount++;
      } catch (e) {
        logToSheet("sendPayslip", `Ошибка отправки фото для ${tabelNo}: ${e.message}`);
      }
    } else {
      notFound = true;
    }
  });

  if (sentPayslipsCount > 0) {
    deleteMessage(chat_id, message_id);
    if (notFound) {
      sendMessage(chat_id, "Некоторые из ваших расчетных листков не были найдены или отправлены.");
    }
  } else {
    editMessage(chat_id, message_id, `Ваш расчетный листок за ${monthName} ${year} не найден.`, null);
  }
}

// ИЗМЕНЕНО: Добавлена кнопка "Назад" для отмены ввода
function handleInfoRequest(chat_id, user_id, message_id) {
  var text = "Введите номер терминала:";
  var keyboard = [[{ text: "⬅️ Назад", callback_data: "back_main" }]];
  var replyMarkup = { inline_keyboard: keyboard };
  
  editMessage(chat_id, message_id, text, replyMarkup);
  setUserState(user_id, { action: 'waiting_for_info_id', prompt_message_id: message_id });
}

// ИЗМЕНЕНО: Кнопки переупорядочены
function handleInfoIdInput(chat_id, user_id, terminalId, userInputMessageId, promptMessageId) {
  clearUserState(user_id);
  deleteMessage(chat_id, userInputMessageId);
  
  var cleanedTerminalId = terminalId.trim();
  var infoData = getInfoById(cleanedTerminalId);
  var responseText;
  var parseMode = "HTML";
  
  var keyboard = [
    [{ text: "⬅️ Назад", callback_data: "back_main" }],
    [{ text: "🔎 Найти еще", callback_data: "main_info" }]
  ];
  var replyMarkup = { inline_keyboard: keyboard };

  function escapeHtml(text) {
      if (typeof text !== 'string' && typeof text !== 'number') return '';
      return text.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  if (infoData) {
    responseText = `<b>Информация о точке</b>\n\n` +
                   `<b>ID:</b> <code>${escapeHtml(infoData.id)}</code>\n` +
                   `<b>Имя:</b> ${escapeHtml(infoData.name)}\n` +
                   `<b>Адрес:</b> ${escapeHtml(infoData.address)}\n` +
                   `<b>ИНО:</b> <code>${escapeHtml(infoData.ino)}</code>`;
  } else {
    responseText = `Терминал с ID <code>${escapeHtml(cleanedTerminalId)}</code> не найден.`;
  }
  
  editMessage(chat_id, promptMessageId, responseText, replyMarkup, parseMode);
}

// ИЗМЕНЕНО: Добавлена кнопка "Назад" для отмены ввода
function handleQrRequest(chat_id, user_id, message_id) {
  deleteMessage(chat_id, message_id);
  
  var text = `Введите значение для QR-кода (до ${CONSTANTS.CONFIG.QR_MAX_LENGTH} символов):`;
  var keyboard = [[{ text: "⬅️ Назад", callback_data: "back_main" }]];
  var replyMarkup = { inline_keyboard: keyboard };
  
  var response = sendMessage(chat_id, text, replyMarkup);
  var responseJson = JSON.parse(response.getContentText());

  if (responseJson.ok) {
    var new_message_id = responseJson.result.message_id;
    setUserState(user_id, { action: 'waiting_for_qr_value', prompt_message_id: new_message_id });
  } else {
    logToSheet("handleQrRequest", `Ошибка отправки сообщения: ${response.getContentText()}`);
  }
}

// ИЗМЕНЕНО: Кнопки переупорядочены
function handleQrValueInput(chat_id, user_id, qrValue, userInputMessageId, promptMessageId) {
  clearUserState(user_id);
  deleteMessage(chat_id, userInputMessageId);

  var cleanedQrValue = qrValue.trim();

  if (cleanedQrValue.length === 0 || cleanedQrValue.length > CONSTANTS.CONFIG.QR_MAX_LENGTH) {
    editMessage(chat_id, promptMessageId, `Ошибка: Длина значения должна быть от 1 до ${CONSTANTS.CONFIG.QR_MAX_LENGTH} символов.`, null);
    return;
  }

  try {
    editMessage(chat_id, promptMessageId, "⏳ Генерирую QR-код...", null);
    var qrApiUrl = "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=" + encodeURIComponent(cleanedQrValue);
    var qrBlob = UrlFetchApp.fetch(qrApiUrl).getBlob().setName("qr_code.png");
    
    var keyboard = [
      [{ text: "⬅️ Назад", callback_data: "back_main" }],
      [{ text: "🔳 Сгенерировать еще", callback_data: "main_qr" }]
    ];
    var replyMarkup = { inline_keyboard: keyboard };
    var caption = `Ваш QR\\-код для значения: \`${cleanedQrValue}\``;
    
    sendPhoto(chat_id, qrBlob, caption, replyMarkup, "MarkdownV2");
    
    deleteMessage(chat_id, promptMessageId);
    logQrRequest(user_id, cleanedQrValue);
  } catch (e) {
    logToSheet("handleQrValueInput", "Ошибка при генерации QR: " + e.toString());
    editMessage(chat_id, promptMessageId, "Произошла ошибка при создании QR-кода. Попробуйте позже.", null);
  }
}

// --- 6. УПРАВЛЕНИЕ СОСТОЯНИЕМ ПОЛЬЗОВАТЕЛЯ ---
function setUserState(user_id, state) {
  userProperties.setProperty(CONSTANTS.KEYS.USER_STATE_PREFIX + user_id, JSON.stringify(state));
}

function getUserState(user_id) {
  var stateJson = userProperties.getProperty(CONSTANTS.KEYS.USER_STATE_PREFIX + user_id);
  return stateJson ? JSON.parse(stateJson) : null;
}

function clearUserState(user_id) {
  userProperties.deleteProperty(CONSTANTS.KEYS.USER_STATE_PREFIX + user_id);
}

// --- 7. ПОЛУЧЕНИЕ ДАННЫХ И КЭШИРОВАНИЕ ---
function getInfoData() {
  var cached = scriptCache.get(CONSTANTS.KEYS.CACHE_INFO_DATA);
  if (cached) {
    return JSON.parse(cached);
  }
  
  var infoSheet = ss.getSheetByName(CONSTANTS.SHEETS.INFO);
  if (!infoSheet) return [];
  var data = infoSheet.getDataRange().getValues();
  if (data.length < 2) return [];
  
  var headers = data[0].map(h => String(h).trim().toLowerCase());
  var idCol = headers.indexOf("id");
  var nameCol = headers.indexOf("name");
  var addressCol = headers.indexOf("address");
  var inoCol = headers.indexOf("ino");

  if (idCol === -1) return [];

  var result = data.slice(1).map(function(row) {
    return {
      id: String(row[idCol]).trim(),
      name: nameCol !== -1 ? row[nameCol] : 'н/д',
      address: addressCol !== -1 ? row[addressCol] : 'н/д',
      ino: inoCol !== -1 ? row[inoCol] : 'н/д'
    };
  });
  
  scriptCache.put(CONSTANTS.KEYS.CACHE_INFO_DATA, JSON.stringify(result), CONSTANTS.CONFIG.CACHE_EXPIRATION_SECONDS);
  return result;
}

function getInfoById(id) {
  var allInfoData = getInfoData();
  return allInfoData.find(item => item.id === id);
}

function getPayslipUsers() {
  var cached = scriptCache.get(CONSTANTS.KEYS.CACHE_PAYSLIP_USERS);
  if (cached) {
    return JSON.parse(cached);
  }

  var payslipSheet = ss.getSheetByName(CONSTANTS.SHEETS.PAYSLIP_USERS);
  if (!payslipSheet) return {};
  var data = payslipSheet.getDataRange().getValues();
  if (data.length < 2) return {};

  var headers = data[0].map(h => String(h).trim().toLowerCase());
  var telegramIdColIdx = headers.indexOf("telegram_id");
  var tabelNoColIdx = headers.indexOf("tabel_no");
  if (telegramIdColIdx === -1 || tabelNoColIdx === -1) return {};

  var userMap = {};
  for (var i = 1; i < data.length; i++) {
    var telegramId = String(data[i][telegramIdColIdx]).trim();
    var tabelNo = String(data[i][tabelNoColIdx]).trim();
    if (telegramId && tabelNo) { 
      if (!userMap[telegramId]) userMap[telegramId] = [];
      userMap[telegramId].push(tabelNo);
    }
  }
  
  scriptCache.put(CONSTANTS.KEYS.CACHE_PAYSLIP_USERS, JSON.stringify(userMap), CONSTANTS.CONFIG.CACHE_EXPIRATION_SECONDS);
  return userMap;
}

function getPayslipCacheData() {
  var cacheSheet = ss.getSheetByName(CONSTANTS.SHEETS.PAYSLIP_CACHE);
  if (!cacheSheet) return [];
  var data = cacheSheet.getDataRange().getValues();
  if (data.length < 2) return [];
  
  var headers = data[0];
  var driveFileNameColIdx = headers.indexOf("Drive_FileName");
  var telegramFileIdColIdx = headers.indexOf("Telegram_FileId");
  var monthYearColIdx = headers.indexOf("Month_Year");

  if (driveFileNameColIdx === -1 || telegramFileIdColIdx === -1 || monthYearColIdx === -1) return [];
  
  return data.slice(1).map(function(row) {
    return {
      driveFileName: String(row[driveFileNameColIdx]).trim(),
      telegramFileId: String(row[telegramFileIdColIdx]).trim(),
      monthYear: String(row[monthYearColIdx]).trim()
    };
  });
}

// --- 8. НИЗКОУРОВНЕВЫЕ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
function getMonthNumber(monthName) {
  var months = ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"];
  return months.indexOf(monthName.toLowerCase());
}

function getMonthNameFromNumber(monthIndex) {
  var months = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
  return (monthIndex >= 0 && monthIndex < 12) ? months[monthIndex] : null;
}

function isUserInGroup(user_id) {
  try {
    var url = telegramUrl + "/getChatMember?chat_id=" + CONSTANTS.PROPERTIES.GROUP_ID + "&user_id=" + user_id;
    var response = UrlFetchApp.fetch(url, { "muteHttpExceptions": true });
    var data = JSON.parse(response.getContentText());
    return (data.ok && ["member", "administrator", "creator"].indexOf(data.result.status) !== -1);
  } catch (e) {
    logToSheet("isUserInGroup", "Ошибка при проверке членства в группе: " + e.message);
    return false;
  }
}

// --- 9. ОБЕРТКИ ДЛЯ TELEGRAM API ---
function apiRequest(method, payload) {
    var options = {
        method: "post",
        contentType: "application/json",
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
    };
    return UrlFetchApp.fetch(telegramUrl + "/" + method, options);
}

function sendMessage(chat_id, text, replyMarkup, parseMode) {
  var payload = { chat_id: String(chat_id), text: text };
  if (replyMarkup) payload.reply_markup = JSON.stringify(replyMarkup);
  if (parseMode) payload.parse_mode = parseMode;
  return apiRequest("sendMessage", payload);
}

function editMessage(chat_id, message_id, text, replyMarkup, parseMode) {
  var payload = { chat_id: String(chat_id), message_id: message_id, text: text };
  if (replyMarkup) payload.reply_markup = JSON.stringify(replyMarkup);
  if (parseMode) payload.parse_mode = parseMode;
  var response = apiRequest("editMessageText", payload);
  logToSheet("editMessage", `API Response for editing msg ${message_id}: ${response.getContentText()}`);
}

function deleteMessage(chat_id, message_id) {
  apiRequest("deleteMessage", { chat_id: String(chat_id), message_id: message_id });
}

function sendPhoto(chat_id, photoContent, caption, replyMarkup, parseMode) {
  var url = telegramUrl + "/sendPhoto";
  var payload = { chat_id: String(chat_id), photo: photoContent };
  
  if (caption) payload.caption = caption;
  if (parseMode) payload.parse_mode = parseMode;
  if (replyMarkup) payload.reply_markup = JSON.stringify(replyMarkup);

  var options = { method: "post", payload: payload, muteHttpExceptions: true };
  var response = UrlFetchApp.fetch(url, options);
  var apiResponse = JSON.parse(response.getContentText()); 
  
  logToSheet("sendPhoto", `API Response: ${response.getContentText()}`);
  if (!apiResponse.ok) {
    throw new Error("Telegram API failure: " + apiResponse.description); 
  }
}

function answerCallbackQuery(callback_query_id) {
  apiRequest("answerCallbackQuery", { callback_query_id: callback_query_id });
}

// --- 10. ФУНКЦИИ НАСТРОЙКИ И ОБСЛУЖИВАНИЯ ---
function logToSheet(funcName, message) {
  if (CONSTANTS.CONFIG.ENABLE_LOGGING !== 1) return;
  try {
    var logSheet = ss.getSheetByName(CONSTANTS.SHEETS.LOGS); 
    if (!logSheet) {
      logSheet = ss.insertSheet(CONSTANTS.SHEETS.LOGS);
      logSheet.appendRow(["Timestamp", "Function", "Message"]);
    }
    logSheet.appendRow([new Date(), funcName, message]);
    if (logSheet.getLastRow() > 1001) {
      logSheet.deleteRows(2, logSheet.getLastRow() - 1001);
    }
  } catch (e) {
    Logger.log("Ошибка записи в лог: " + e.message);
  }
}

function logQrRequest(user_id, value) {
  logToSheet("QR_GENERATION", `*QR* | UserID: ${user_id} | Value: ${value}`);
}

function syncPayslipsWithTelegram() {
  logToSheet("syncPayslipsWithTelegram", "Starting synchronization...");
  var payslipFolder;
  try {
    payslipFolder = DriveApp.getFolderById(CONSTANTS.PROPERTIES.PAYSLIP_FOLDER_ID);
  } catch (e) {
    logToSheet("syncPayslipsWithTelegram", "Ошибка доступа к папке: " + e.message);
    return;
  }
  
  var currentDriveFiles = {}; 
  var filesIterator = payslipFolder.getFilesByType(MimeType.PNG);
  while (filesIterator.hasNext()) {
    var file = filesIterator.next();
    var fileName = file.getName();
    if (fileName.match(/^(\d{4})-(\d{2})-\d+\.png$/)) {
      currentDriveFiles[fileName] = file.getId();
    }
  }
  
  var payslipCacheSheet = ss.getSheetByName(CONSTANTS.SHEETS.PAYSLIP_CACHE);
  if (!payslipCacheSheet) {
    logToSheet("syncPayslipsWithTelegram", "Лист 'payslip_cache' не найден.");
    return;
  }
  
  var cachedData = getPayslipCacheData(); 
  var cachedFileNames = new Set(cachedData.map(row => row.driveFileName));
  var newCacheEntries = [];

  for (var fileName in currentDriveFiles) {
    if (currentDriveFiles.hasOwnProperty(fileName) && !cachedFileNames.has(fileName)) {
        var fileIdInDrive = currentDriveFiles[fileName];
        var fileBlob = DriveApp.getFileById(fileIdInDrive).getBlob();
        var monthYearMatch = fileName.match(/^(\d{4})-(\d{2})-\d+\.png$/);
        var monthYear = monthYearMatch ? `${getMonthNameFromNumber(parseInt(monthYearMatch[2], 10) - 1)} ${monthYearMatch[1]}` : 'Unknown';
        
        try {
          var response = apiRequest("sendPhoto", {
            chat_id: CONSTANTS.PROPERTIES.TELEGRAM_CACHE_CHANNEL_ID,
            photo: fileBlob,
            caption: `Расчетный листок: ${fileName}`
          });
          var telegramApiResponse = JSON.parse(response.getContentText());

          if (telegramApiResponse.ok && telegramApiResponse.result.photo) {
            var telegramFileId = telegramApiResponse.result.photo.pop().file_id;
            newCacheEntries.push([fileName, telegramFileId, monthYear, new Date().toISOString()]);
            logToSheet("syncPayslipsWithTelegram", `Uploaded ${fileName}.`);
          }
        } catch (e) {
          logToSheet("syncPayslipsWithTelegram", `Error uploading ${fileName}: ${e.message}`);
        }
    }
  }
  
  var filesToDeleteFromCache = cachedData
    .filter(row => !currentDriveFiles.hasOwnProperty(row.driveFileName))
    .map(row => row.driveFileName);
  
  if (newCacheEntries.length > 0) {
    payslipCacheSheet.getRange(payslipCacheSheet.getLastRow() + 1, 1, newCacheEntries.length, newCacheEntries[0].length).setValues(newCacheEntries);
  }
  
  if (filesToDeleteFromCache.length > 0) {
    var allCacheValues = payslipCacheSheet.getDataRange().getValues();
    var newCacheValues = allCacheValues.filter(row => !filesToDeleteFromCache.includes(row[0]));
    payslipCacheSheet.clearContents();
    if (newCacheValues.length > 0) {
      payslipCacheSheet.getRange(1, 1, newCacheValues.length, newCacheValues[0].length).setValues(newCacheValues);
    }
  }
  
  logToSheet("syncPayslipsWithTelegram", "Synchronization completed.");
}

function setWebhook() {
  var webAppUrl = ScriptApp.getService().getUrl();
  var response = UrlFetchApp.fetch(telegramUrl + "/setWebhook?url=" + webAppUrl);
  logToSheet("setWebhook", response.getContentText());
  Logger.log(response.getContentText());
}

function setMyCommands() {
  var commands = [{ "command": "start", "description": "Показать главное меню" }];
  var response = apiRequest("setMyCommands", { "commands": commands });
  logToSheet("setMyCommands", response.getContentText());
  Logger.log(response.getContentText());
}
есть что улучшить соптимизировать не теряя функционал и вид меню