import { EventEmitter } from 'events';
import { v4 as uuidv4 } from 'uuid';
import * as WebSocket from 'websocket';
import { catchError, from, fromEvent, of } from 'rxjs';
import { concatMap, finalize, filter, timeout, take } from 'rxjs/operators';
import { CronJob } from 'cron';

import {
  infoLogger,
  logger,
  loggerMessage,
  loggerStatistic,
} from './config/logger/winston';

import type {
  DataModule,
  ImagesDataAll,
  settingsDataStructure,
  FuelStationData,
  OptionsDataStructure,
  FpStatusMessage,
  BasketUpdateMessage,
  AddFuelLine,
  AddPostPayFuel,
  addProductLine,
  AddProductLine,
  AddProductLineByBarCode,
  AnswerAddAcceptOrder,
  AnswerShowMessage,
  AddRequestCallback,
  AddLoyaltyOtpCode,
  AddResendOtpTimerValue,
  AddShowMessageWindow,
  AnswerServiceUnavailable,
  AnswerServiceAvailable,
  AnswerConnected,
  IFpLock,
  ShiftMassage,
  PingPong,
  PostProductInfo,
  AddCallbackClientData,
} from './types';
import ButtonsOtp from './shared/const';

const ksoEmitter = new EventEmitter(); // Создаем новый экземпляр EventEmitter
const arrUuidKey: string[] = []; // Массив с уникальными ключами, необходимы для подтверждения актуальности в запросах от клиента к серверу

let sendBucketUpdate = true;

let isPingPongEnabled = false;

let ws: WebSocket.client | null = null; // Объявляем переменную ws здесь, чтобы она была доступна в других функциях
let connectionEstablished = false; // Флаг на проверку ws соединения (костыль)
let crmIdLoyalty: string; // Флаг на проверку ws соединения (костыль)
let pingPongId: string;
let isPingPongResponce = true;

/** Объект со всеми данными */
const dataModule: DataModule = {
  menuCafeServiceData: null, // Объект с меню кафе
  imagesDataAllData: null, // Массив со списком изображений два типа(type) изображение(1 – группы, 2 - товара)
  settingsDataStructureData: null, // Объект с объектом pos, внутри имя и идентификатор КСО
  fuelStationDataData: null, // Объект с двумя свойствами - fps и tankProducts (два массива, в первом перечисляются колонки
  // (id и массив с пистолетами), во втором баки с топливом(объекты с описанием бака)
  optionsDataStructureData: null, // Объект с различными таймаутами(отображения окна с сообщением, отображения сообщения,
  // отображения краткого сообщения, ожидания действий пользователя, сообщения “Продолжить ожидание?”)
  // и кол-ва биперов
  informationMessageFromKSO: '', // Переменная, куда записываются информационные сообщения от сервера
  //? Возможно сделать массивом с объектами, внутри которых будут записываться все сообщения
  remainingTimeValue: 0, // Переменная с информацией о остатки времени в секундах, после которого можно запросить код повторно
  confirmationPointsDebiting: '', // Переменная с извещением о введением неверного кода подтверждения списания баллов
  answerButtons: null, // Массив с кнопками
  arrStateTRK: [], // Массив с объектами, внутри которых состояние ТРК(колонки)
  acceptOrderValue: false, // Подтверждение заказа
  valueFuel: null, // Текущие значение(id) снятого пистолета
  basketValue: null, // Объект с состоянием корзины
  idClient: null, // id клиента
  clientData: null,
};

/** Функция для получения объект со всеми данными */
const getDataModule = (): DataModule => {
  return dataModule;
};

/** Подключение оповещений(эмиттерев) */
const on = (event: string | symbol, action: (data: any) => void) => {
  ksoEmitter.on(event, (data: string) => action(data));
};

// Функция для отправки сообщения на вебсокет
function sendMessage(wsc: WebSocket.connection, message: string) {
  logger.log('debug', message, ['request']);

  if (wsc.connected) {
    wsc.send(message);
  }
}

/** Функция будет обрабатывать сообщения, полученные через WebSocket соединение */
function onMessage(msg: any) {
  const data:
    | ImagesDataAll
    | settingsDataStructure
    | FuelStationData
    | OptionsDataStructure
    | FpStatusMessage
    | BasketUpdateMessage
    | AddFuelLine
    | AddPostPayFuel
    | AddProductLine
    | AddProductLineByBarCode
    | AnswerAddAcceptOrder
    | AnswerShowMessage
    | AddRequestCallback
    | AddLoyaltyOtpCode
    | AddResendOtpTimerValue
    | AddShowMessageWindow
    | AnswerServiceUnavailable
    | AnswerServiceAvailable
    | AnswerConnected
    | ShiftMassage
    | AddCallbackClientData
    | PingPong
    | PostProductInfo
    | IFpLock = JSON.parse(msg.utf8Data); //! String(msg)
  if (data) {
    logger.debug(JSON.stringify(data), ['response']);

    switch (data.msgType) {
      case 'imagesDataAll':
        infoLogger.info(JSON.stringify(data), ['response']);
        dataModule.imagesDataAllData = data.pictures;
        //? Возможно переделка данных, в одно стороне изображения групп, в другой товаров
        ksoEmitter.emit('info', 'Изображения товаров и групп получены');
        ksoEmitter.emit('imagesDataAll', data.pictures);
        break;
      case 'stationConfig':
        dataModule.settingsDataStructureData = data.data;
        //? Понять для чего нужны эти данные
        ksoEmitter.emit('info', 'Конфигурация КСО получены');
        ksoEmitter.emit('stationConfig', data.data);
        break;
      case 'fpConfig':
        console.log(data);

        const objectConfig = {
          fps: data.fps,
          tankProducts: data.tankProducts,
        };
        dataModule.fuelStationDataData = objectConfig;
        ksoEmitter.emit('fpConfig', objectConfig);
        //? Возможно изменить структуру, в nozzles вместо tankId использовать непосредственно объект из tankProducts
        ksoEmitter.emit('info', 'Конфигурация ТРК получены');
        break;
      case 'fpLock':
        infoLogger.info(JSON.stringify(data), ['response']);
        ksoEmitter.emit('info', 'ТРК заблокирована');
        break;
      case 'optionsUpdate':
        //? Понять для чего нужны эти данные
        dataModule.optionsDataStructureData = data.data;
        ksoEmitter.emit('info', 'Настройки АЗС получены');
        ksoEmitter.emit('optionsUpdate', data.data);
        break;
      case 'fpStatus':
        // Избавляемся от лишнего сообщения(msgType)
        const { msgType, data: objectWithoutMsgType } = data;
        const index = dataModule.arrStateTRK.findIndex(
          (el) => el.fpId === objectWithoutMsgType.fpId,
        );
        //? Проверить, отрабатывает ли
        if (index !== -1) {
          // Если статус был, для этой колонки(переписываем)
          dataModule.arrStateTRK[index] = objectWithoutMsgType;
        } else {
          // Если не было пушим новый
          dataModule.arrStateTRK.push(objectWithoutMsgType);
          ksoEmitter.emit('info', `Получено состояние ТРК ${data.data.fpId}`);
        }
        ksoEmitter.emit('fpStatus', dataModule.arrStateTRK);
        // Создать новую подпись на имитацию налива
        break;
      case 'basketUpdate':
        infoLogger.info(JSON.stringify(data), ['response']);
        // Корзина обновляется после каждого добавления товара или налива, удаления\товара, отмены заказа
        dataModule.basketValue = data.data;

        if (!sendBucketUpdate) {
          ksoEmitter.emit('info', 5);
        }

        if (data.data.basketState === 3 && dataModule.basketValue.cheque !== null) {
          loggerStatistic.log(
            'statistics',
            JSON.stringify({
              data,
              timestamp: new Date().toUTCString(),
            }),
          );
        }

        // Проверка на пустой объект, если в массиве нет поля cheque, то не отправляем на клиент
        if (
          dataModule?.basketValue?.basketState === 3 &&
          dataModule.basketValue.cheque === null
        ) {
          break;
        }

        if (sendBucketUpdate) {
          ksoEmitter.emit('info', 'Корзина обновлена');
          ksoEmitter.emit('basketUpdate', data.data);
        }

        if (dataModule?.basketValue?.basketState === 3) {
          ksoEmitter.emit('info', 'Оплата прошла успешно');
          // Отключаем флаг подтверждения заказа
          dataModule.acceptOrderValue = false;
          ksoEmitter.emit('paymentSuccessful', true); //! Подписаться на изменения
          arrUuidKey.length = 0;

          const indexUnlockValue = dataModule.arrStateTRK.findIndex(
            (el) => el.logicalNozzleId === dataModule.valueFuel,
          );

          if (dataModule.basketValue.cheque.lines.find((el) => el.nozzle !== null)) {
            fpUnlock(
              dataModule.arrStateTRK[indexUnlockValue].fpId,
              dataModule.arrStateTRK[indexUnlockValue].lockId,
              dataModule.arrStateTRK[indexUnlockValue].logicalNozzleId,
            );
          }
        }
        break;
      case 'addFuelLine':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data && arrUuidKey.includes(data.cmdId)) {
          ksoEmitter.emit(
            'info',
            `Получено подтверждение на добавление предоплатного налива`,
          );
          ksoEmitter.emit('fuelPre', true); //? Возможно подпись не нужна
        } else {
          ksoEmitter.emit(
            'error',
            `В подтверждение на добавление предоплатного налива произошла ошибка по cmdId ${data.cmdId}`,
          );
        }
        break;
      case 'addPostpayFuel':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data && arrUuidKey.includes(data.cmdId)) {
          ksoEmitter.emit(
            'info',
            `Получено подтверждение на добавление постоплатного налива по cmdId ${data.cmdId}`,
          );
          ksoEmitter.emit('fuelPost', true); //? Возможно подпись не нужна
        } else {
          ksoEmitter.emit(
            'error',
            `В подтверждение на добавление постоплатного налива произошла ошибка по cmdId ${data.cmdId}`,
          );
        }
        break;
      case 'addProductLine':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data === false) {
          logger.error(
            `Произошла ошибка при добавлении товара в корзину (Товар в корзину не добавлен)`,
          );
          ksoEmitter.emit(
            'error',
            'Произошла ошибка при добавлении товара в корзину (Товар в корзину не добавлен)',
          );
          return;
        }
        ksoEmitter.emit(
          'info',
          `Получено подтверждение на добавление товара из меню КСО`,
        );
        ksoEmitter.emit('product', true); //? Возможно подпись не нужна
        break;
      case 'addProductLineByBarCode':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data === null) {
          ksoEmitter.emit(
            'info',
            `Получено подтверждение на добавление товара через сканер`,
          );
          break;
        }

        if (data.data) {
          ksoEmitter.emit('info', data.data);
          break;
        }

        ksoEmitter.emit('info', 'С касу пришло неизвестное сообщение');
        ksoEmitter.emit('productScan', true); //? Возможно подпись не нужна

        break;
      case 'acceptOrder':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          ksoEmitter.emit(
            'info',
            `Получено подтверждение на добавление оплаты, номер заказа ${data.data}`,
          );
          ksoEmitter.emit('acceptOrderCheck', data.data);
        } else {
          ksoEmitter.emit('error', `Заказ не добавлен, номер заказа не получен`);
        }
        break;
      case 'showMessage':
        loggerMessage.log('message', data.data);
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          dataModule.informationMessageFromKSO = data.data;
          ksoEmitter.emit('info', `Получено информационное сообщение - ${data.data}`);
          ksoEmitter.emit('arrivalMessage', data.data); //! Здесь надо организовать правильный подход в работе всех сообщений под этой подписью
        } else {
          ksoEmitter.emit('error', `Заказ не добавлен, номер заказа не получен`);
        }
        break;
      case 'requestCallback':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          dataModule.answerButtons = data;
          dataModule.informationMessageFromKSO = data.data.title;
          ksoEmitter.emit(
            'info',
            `Получено массив с кнопками не/подтверждение списание баллов(btnYes,btnNo)`,
          );
          ksoEmitter.emit('addButtons', data.data.buttons);
          ksoEmitter.emit('informationMessageFromKSO', data.data?.title);
          ksoEmitter.emit('informationMessageFromKSO', data.data?.message);
        } else {
          ksoEmitter.emit('error', `Массив с кнопками не получен`);
        }
        break;
      case 'loyaltyOtpCode':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          dataModule.informationMessageFromKSO = data.data;
          crmIdLoyalty = data.cmdId;
          ksoEmitter.emit('arrivalOtpCode', data.data);
          ksoEmitter.emit(
            'info',
            `Получено сообщение о необходимо подтверждение списания баллов`,
          );
        } else {
          ksoEmitter.emit(
            'error',
            `Сообщение о необходимо подтверждение списания баллов не получено`,
          );
        }
        break;
      case 'showMessageWindow':
        loggerMessage.log('message', data.data);
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          dataModule.confirmationPointsDebiting = data.data;
          ksoEmitter.emit('showMessageWindow', data.data);
        }
        break;
      case 'callbackClientData':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          dataModule.idClient = data.data.idClient;
          dataModule.clientData = data.data;
          ksoEmitter.emit('callbackClientData', data.data.idClient);
          ksoEmitter.emit('info', data.data);
        }
        break;
      case 'resendOtpTimerValue':
        infoLogger.info(JSON.stringify(data), ['response']);
        if (data.data) {
          dataModule.remainingTimeValue = data.data;
          ksoEmitter.emit('arrivalOtpTimerValue', data.data);
          ksoEmitter.emit(
            'info',
            `Получено значение с остатком времени в секундах, после которого можно запросить код повторно - ${dataModule.remainingTimeValue}`,
          );
        } else {
          ksoEmitter.emit(
            'error',
            `Значение с остатком времени в секундах, после которого можно запросить код повторно не получено`,
          );
        }
        break;
      case 'connected':
        ksoEmitter.emit('info', `Connected with server`);
        break;
      case 'serviceUnavailable':
        ksoEmitter.emit('info', `СЕРВИС НЕДОСТУПЕН\u000d\u000aНЕТ СВЯЗИ С ККМ`);
        ksoEmitter.emit('serviceAvailableCheck', false);
        break;
      case 'serviceAvailable':
        ksoEmitter.emit('info', `СЕРВИС ДОСТУПЕН`);
        ksoEmitter.emit('serviceAvailableCheck', true);
        break;
      case 'shiftOpened':
        ksoEmitter.emit('shift', 'Смена открыта');
        break;
      case 'shiftClosed':
        ksoEmitter.emit('shift', `Смена закрыта`);
        break;

      case 'pong':
        if (pingPongId === data.cmdId) isPingPongResponce = true;
        break;

      case 'postProductInfo':
        const {
          cmdId,
          price: { value },
          data: { externalID },
          quantity,
          name,
        } = data;

        ksoEmitter.emit('postProductInfo', {
          cmdId,
          name,
          externalId: externalID,
          price: value,
          quantity,
        });
        break;

      default:
        ksoEmitter.emit('error', data);
        break;
    }
  }
}

/** Функция для установления websocket соединения и получения всех первичных и основных данных , кроме меню*/
function connect(urlWs: string, ClientProgramValue: string, ClientNameValue: string) {
  if (!validateConnectParams(urlWs, ClientNameValue)) {
    return;
  }
  const headers = {
    ClientProgram: ClientProgramValue,
    ClientName: ClientNameValue,
  };
  createWebSocket(urlWs, headers, ClientProgramValue, ClientNameValue);
}

/** Функция для получения меню КАСУ с помощью HTTP запроса */
function fetchMenuKASU(urlHttp: string) {
  if (!urlHttp) {
    ksoEmitter.emit(
      'error',
      'Не был передан urlHttp (url для Http соединения - получения меню)',
    );
    return false;
  }
  fetch(urlHttp)
    .then((response) => {
      if (!response.ok) {
        throw new Error('Ошибка HTTP запроса: ' + response.status);
      }
      return response.json();
    })
    .then((data) => {
      dataModule.menuCafeServiceData = data;
      ksoEmitter.emit('menuData', {
        type: 'Menu',
        dataModule: dataModule.menuCafeServiceData,
      });
    })
    .catch((error) => {
      ksoEmitter.emit('error', `HTTP запрос на получение меню не удался: ${error}`);
    });
}

/** Функция создает webSocket и слушает его события */
function createWebSocket(
  urlWs: string,
  headers: any,
  ClientProgramValue: string,
  ClientNameValue: string,
) {
  // Создаем экземпляр WebSocket
  ws = new WebSocket.client();

  // Обработка события успешного подключения
  ws.on('connect', (connection: WebSocket.connection) => {
    connectionEstablished = true;
    console.log('Успешное подключение');

    connection.on('close', () => {
      console.log('Connection close');
      ksoEmitter.emit('error', `Connection close`);
      ksoEmitter.off('sendMessageKSO', addSendMessage);
      // Переподключение, если произошёл разрыв
      connect(urlWs, ClientProgramValue, ClientNameValue);
    });

    connection.on('error', function (error) {
      logger.error(`Ошибка в соединении: ${error.toString()}`);
      ksoEmitter.emit('error', `Ошибка в соединении: ${error.toString()}`);
    });

    connection.on('message', (message) => {
      onMessage(message);
    });

    ksoEmitter.on('sendMessageKSO', (message) => sendMessage(connection, message));
    addSendMessage({ msgType: 'imagesDataAll' });
  });

  // Обработка события неудачного подключения
  ws.on('connectFailed', (error: Error) => {
    connectionEstablished = false;
    logger.error(`Ошибка при подключении WebSocket: ${error.toString()}`);
    ksoEmitter.emit('error', `Ошибка при подключении WebSocket: ${error.toString()}`);
  });

  // Подключаемся к WebSocket с настройками
  ws.connect(urlWs, undefined, undefined, headers);
}

/** Функция для отправки сообщения на сервер(сразу переделывает в json) */
function addSendMessage(message: Record<string, any>) {
  if (!connectionEstablished) {
    console.error('WebSocket-соединение не установлено');
    logger.error('WebSocket-соединение не установлено');

    return;
  }
  if (!ws) {
    logger.error('WebSocket-соединение не установлено');
    ksoEmitter.emit('error', 'WebSocket-соединение не установлено');
    return;
  }
  const sendMessage = JSON.stringify(message);
  console.log('sendMessage', sendMessage);

  ksoEmitter.emit('sendMessageKSO', sendMessage);
}

/** Функция будет проверять переданные параметры на наличие их значений */
function validateConnectParams(urlWs: string, valueKCO: string) {
  if (!urlWs) {
    logger.error('Не был передан urlWs (url для WebSocket соединения)');
    ksoEmitter.emit('error', 'Не был передан urlWs (url для WebSocket соединения)');
    return false;
  }
  if (!valueKCO) {
    logger.error('Не был передан valueKCO (имя КСО)');

    ksoEmitter.emit('error', 'Не был передан valueKCO (имя КСО)');
    return false;
  }
  return true;
}

/** Функция для добавление уникальных ключей в массив*/
function addIdIfNotExists(id: string): void {
  if (!arrUuidKey.includes(id)) {
    arrUuidKey.push(id);
  } else {
    const newId = uuidv4();
    arrUuidKey.push(newId);
  }
}

/** функция для добавления продукта из меню КСО в корзину */
function addProductToCart(productId: number, price: number) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent: addProductLine = {
    msgType: 'addProductLine',
    cmdId: id,
    data: {
      productId,
      price,
      inputType: 11, // Взято из протокола КСО-КАСУ
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция добавления товара с помощью скана */
function addProductScan(code: string) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'addProductLineByBarCode',
    cmdId: id,
    data: {
      code,
      inputType: 10, // Взято из протокола КСО-КАСУ
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent); // Отправляем сообщение на сервер
}

/** Функция для удаления товара(Идентификатор строки заказа) */
//? Пока не ясно, это только для топлива или ещё для товаров с кафе, т.к. extChequeId есть в объеках массива arrStateTRK, а это от сообщения сервера fpStatus
function deleteProduct(chequeLineId: number) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'deleteLine',
    cmdId: id,
    data: {
      chequeLineId,
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для предоплатного налива */
function addFuelLine(nozzleId: number, isFuellingInMoney: boolean, quantity: number) {
  const id = uuidv4();
  if (!dataModule.fuelStationDataData) {
    return null;
  }
  addIdIfNotExists(id);

  // Функция для нахождения id бака с топливом
  function findTankIdById(id: number) {
    if (!dataModule.fuelStationDataData) {
      return null;
    }
    for (const fps of dataModule.fuelStationDataData.fps) {
      for (const nozzle of fps.nozzles) {
        if (nozzle.id == id) {
          return nozzle.tankId;
        }
      }
    }
    return null; // Если id не найден
  }

  // Здесь определяем сумму, если isFuellingInMoney === true, просто передаём сумму из amountDose, если нет вычисляем объём топливо * цену из ранее полученных данных о ТРК
  let valueSum;
  let finalQuantity;
  const tankId = findTankIdById(nozzleId);
  const priceFuel = dataModule.fuelStationDataData.tankProducts.find(
    (el) => el.tankId === tankId,
  )?.product.price;

  if (isFuellingInMoney && priceFuel) {
    valueSum = quantity;
    finalQuantity = (quantity / priceFuel).toFixed(2);
  } else {
    if (priceFuel) {
      valueSum = (priceFuel * quantity).toFixed(2);
      finalQuantity = quantity;
    }
  }
  const objectToBeSent = {
    msgType: 'addFuelLine',
    cmdId: id,
    data: {
      nozzleId,
      quantity: finalQuantity,
      amountDose: valueSum,
      isFuellingInMoney,
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для постоплатного налива */
function addPostPayFuel(fpId: number) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'addPostpayFuel',
    cmdId: id,
    data: {
      fpId,
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для отмены всего заказа (очищает корзину) */
function cancelOrder() {
  const id = uuidv4();
  addIdIfNotExists(id);

  const objectToBeSent = {
    msgType: 'cancelOrder',
    cmdId: id,
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция блокировки ТРК */
function fpLock(fpId: number, lockId: number, nozzleId: number) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'fpLock',
    cmdId: id,
    data: {
      fpId,
      lockId,
      nozzleId,
    },
  };

  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция разблокировки ТРК */
function fpUnlock(fpId: number, lockId: number, nozzleId: number) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'fpUnlock',
    cmdId: id,
    data: {
      fpId,
      lockId,
      nozzleId,
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для подтверждения заказа */
function acceptOrder(clientPhoneNumber: string, beeperNumber: string, takeAway: boolean) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'acceptOrder',
    cmdId: id,
    data: {
      clientPhoneNumber,
      beeperNumber,
      takeAway,
    },
  };

  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для отправки на сервер параметров оплаты заказа
 * @param {number} sellingTypeId - sellingTypeId.
 * @param {boolean} applyDiscount - true если необходимо применить карту лояльности.
 * @param {boolean} isLicardCard - true если оплата топливной картой.
 * @param {string} loyaltyCardNumber - Номер карты лояльности, запрошенной ранее через qr-код.
 * @param {0 | 1 | 2} loyaltyRequestType - Способ считывания qr-кода: 0 – неизвестно, 1 - сканер, 2 – терминал.
 * */
function startPay(
  sellingTypeId: number,
  applyDiscount: boolean,
  isLicardCard: boolean,
  loyaltyCardNumber: string,
  loyaltyRequestType: 0 | 1 | 2,
) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'startPay',
    cmdId: id,
    data: {
      sellingTypeId,
      applyDiscount,
      isLicardCard,
      loyaltyCardNumber,
      loyaltyRequestType,
    },
  };
  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для отправки на сервер сообщение о применении карты лояльности с QR кодом или clientId*/
function processQrLoyalty({ qrCode, idClient }: { qrCode?: string; idClient?: string }) {
  const id = uuidv4();
  addIdIfNotExists(id);

  if (qrCode && idClient) {
    ksoEmitter.emit('error', 'Передан и qrCode и clintId');
    logger.error('Передан и qrCode и clintId');
    return;
  }

  const objectToBeSent = {
    msgType: 'processQrLoyalty',
    cmdId: id,
    data: {
      qrCode,
      idClient,
    },
  };

  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для отправки на сервер нажатой кнопки(не/подтверждающий списание баллов) */
function pressedButton(pressButton: string) {
  if (!dataModule.answerButtons) {
    return;
  }

  const buttonsData = dataModule.answerButtons;

  const choiceButton = buttonsData.data.buttons.find((el) => el.name === pressButton);

  const objectToBeSent = {
    msgType: 'requestCallback',
    cmdId: buttonsData.cmdId,
    data: {
      pressedButton: {
        name: choiceButton?.name,
        text: choiceButton?.text,
        default: choiceButton?.default,
      },
    },
  };

  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для отправки на сервер сообщения с введённым кодом
 *  @param {Object} data - Объект
 *  @param {ButtonsOtp} data.pressedButtons - название нажатой кнопки.
 *  @param {number} data.otpCode - OTP код.
 */
function loyaltyOtpCode(data?: { pressedButtons?: ButtonsOtp; otpCode?: number }) {
  const pressedButtons = data?.pressedButtons || ButtonsOtp.BtnContinue;
  const otpCode = data?.otpCode;

  const objectToBeSent = {
    msgType: 'loyaltyOtpCode',
    cmdId: crmIdLoyalty,
    data: {
      pressedButton: { name: pressedButtons },
      otpCode,
    },
  };

  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для запроса данных клиента (отправляем qr-лояльности) */
function getClientData(qrCode: string) {
  const id = uuidv4();
  addIdIfNotExists(id);
  const objectToBeSent = {
    msgType: 'getClientData',
    cmdId: id,
    data: {
      qrCode: qrCode,
    },
  };

  infoLogger.info(JSON.stringify(objectToBeSent), ['request']);
  addSendMessage(objectToBeSent);
}

/** Функция для запроса полученных с КАСУ кнопок (из полученных кнопок отправляем name при помощи функции pressedButton) */
function getLoyaltyButtons(): DataModule['answerButtons'] {
  return dataModule.answerButtons;
}

/** Функция для запроса полученных с КАСУ данных о пользователе */
function getClient(): DataModule['clientData'] {
  return dataModule.clientData;
}

/** Функция для отмены всего заказа (очищает корзину) */
function cancelOrderWithRemoveCheckLines() {
  const id = uuidv4();
  addIdIfNotExists(id);

  const checkLinesId = dataModule.basketValue?.cheque?.lines.reduce<number[]>(
    (prev, current) => {
      // if (current.nozzle?.id) {
      //   return prev;
      // }

      prev.push(current.id);

      return prev;
    },
    [],
  );

  checkLinesId?.forEach((lineId) => {
    deleteProduct(lineId);
  });

  const objectToBeSent = {
    msgType: 'cancelOrder',
    cmdId: id,
  };

  addSendMessage(objectToBeSent);
}

/** Функция для отмены ожидания сканирования qr */
function resetTransaction() {
  const objectToBeSent = {
    msgType: 'resetTransaction',
    data: {
      chequeId: dataModule.basketValue?.cheque.id,
    },
  };

  addSendMessage(objectToBeSent);
}

function getProductInfo(externalId: number) {
  const id = uuidv4();

  const objectToBeSent = {
    msgType: 'getProductInfo',
    cmdId: id,
    data: {
      externalID: externalId,
    },
  };

  addSendMessage(objectToBeSent);
}

/** Функция для удаления топлива из корзины, и восстановления остальных товаров в корзине */
function cancelOrderAndRecoverBasket(): void {
  const externalIds = dataModule.basketValue?.cheque?.lines.reduce<
    { id: number; price: number }[]
  >((prev, current) => {
    if (current.nozzle?.id) {
      return prev;
    }

    prev.push({
      id: current.product.id,
      price: current.product.price,
    });

    return prev;
  }, []);

  sendBucketUpdate = false;

  cancelOrder();

  ksoEmitter?.once('info', () => {
    const ids = from(externalIds || []);

    return ids
      .pipe(
        concatMap(async ({ id, price }) => {
          addProductToCart(id, price);

          await waitBucketUpdateWithStatusTwo();
          catchError((err: Error) => {
            return of({ id, price });
          });
          timeout(200);
        }),

        finalize(() => {
          sendBucketUpdate = true;
        }),
      )
      .subscribe();
  });
}

function waitBucketUpdateWithStatusTwo() {
  return fromEvent<number>(ksoEmitter, 'info')
    .pipe(
      filter((value) => {
        return value === 5;
      }),
      take(1),
    )
    .toPromise();
}

function setPingPongEnabled(enabled: boolean) {
  isPingPongEnabled = enabled;
}

new CronJob(
  '0,30 * * * * *',
  () => {
    if (isPingPongEnabled) {
      const id = uuidv4();
      const objectToBeSent = {
        msgType: 'ping',
        cmdId: id,
        data: null,
      };

      pingPongId = id;
      isPingPongResponce = false;
      addSendMessage(objectToBeSent);
    }
  },
  null,
  true,
);

new CronJob(
  '15,45 * * * * *',
  () => {
    if (isPingPongEnabled) {
      if (!isPingPongResponce) {
        logger.error(`Касу не ответило на ping`);
        ksoEmitter.emit('ping', `Касу не ответило на ping`);
      } else {
        logger.error(`Касу ответило на ping`);
        ksoEmitter.emit('ping', `Касу ответило на ping`);
      }
    }
  },
  null,
  true,
);

export {
  connect,
  addProductToCart,
  addProductScan,
  deleteProduct,
  addFuelLine,
  addPostPayFuel,
  cancelOrder,
  fpLock,
  fpUnlock,
  acceptOrder,
  startPay,
  processQrLoyalty,
  pressedButton,
  loyaltyOtpCode,
  on,
  getDataModule,
  fetchMenuKASU,
  getClientData,
  getLoyaltyButtons,
  getClient,
  cancelOrderAndRecoverBasket,
  cancelOrderWithRemoveCheckLines,
  resetTransaction,
  setPingPongEnabled,
  getProductInfo,
};
