import VueRouter from 'vue-router';
import { Cookies, Notify } from 'quasar';
import Vue from 'vue';
import store from '@state/store';
import { router } from '@/router';
import { i18n } from '@plugins/i18n';
import get from 'lodash/get';
import isObject from 'lodash/isObject';
import { CardNeedToUpdateType } from '@constants/models/Card';

export enum MessageEventDataBodyType {
  ERROR = 'error',
  INFO = 'info',
  PROCESS = 'process',
  SUBMIT = 'submit',
  SUCCESS = 'success',
  TASK = 'task',
  WARNING = 'warning',
}

enum MessageEventDataAction {
  error = 'error',
  success = 'success',
}

interface MessageEventData {
  id?: string;
  traceId?: string;
  createDate?: string;
  till?: string;
  nextTry?: string;
  attempts?: number;
  forUser?: string;
  armID?: string;
  className?: string;
  action?: MessageEventDataAction;
  body?: any;
}

interface WebsocketsConfig {
  router: VueRouter;
  url: string;

  protocols?: string | string[];
}

export const websockets: {
  instance: WebSocket | null;
} = {
  instance: null,
};

function initCookie(): Promise<void> {
  return new Promise((resolve, reject) => {
    const currentDate = new Date();

    currentDate.setDate(currentDate.getDate() + 1);

    const createCookie = () => {
      store
        .dispatch('oidcClient/getAccessToken')
        .then((accessToken) => {
          Cookies.set('JWT', accessToken, {
            expires: currentDate.toUTCString(),
            path: '/',
            secure: true,
          });

          return resolve();
        })
        .catch((e) => reject(e));
    };

    try {
      createCookie();
    } catch (e) {
      setTimeout(() => {
        createCookie();
      }, 2000);
    }
  });
}

function resetCookie(): void {
  Cookies.remove('JWT', {
    path: '/',
  });
}

/**
 * Функция получения строки из ответа вебсокета
 * @param websocketMsg - может быть строка, может быть объект с полем message, а может быть еще что-то
 * @return строка с текстом
 */
export function getWebsocketsMsg(
  websocketMsg: unknown,
  defaultMsg: string = i18n.t('websockets.noMessage').toString()
): string {
  if (isObject(websocketMsg)) {
    return get(websocketMsg, 'message', defaultMsg);
  }
  return String(websocketMsg) || defaultMsg;
}

/**
 * Функция является основной для обработки пушей.
 * @param e - Объект сообщения в websocket-канале.
 */
function defaultListener(e: MessageEvent): void {
  const data: MessageEventData = JSON.parse(e.data);
  const currentRoute = router.currentRoute;

  switch (data.body.type) {
    case MessageEventDataBodyType.ERROR: {
      Notify.create({
        color: 'negative',
        icon: 'error',
        message: getWebsocketsMsg(data.body.msg),
      });

      break;
    }
    case MessageEventDataBodyType.INFO: {
      if (data.body.msg) {
        Notify.create({
          color: 'info',
          icon: 'info',
          message: getWebsocketsMsg(data.body.msg, ''),
        });
      }

      switch (currentRoute.name) {
        case 'cards': {
          if (
            get(currentRoute, 'params.id') === get(data, 'body.statementId')
          ) {
            store.commit(
              'utils/cards/needToUpdate',
              CardNeedToUpdateType.MODEL
            );
          }

          break;
        }
      }

      break;
    }
    case MessageEventDataBodyType.PROCESS: {
      switch (currentRoute.name) {
        case 'stepper': {
          if (data.body.description) {
            router.push({
              path: data.body.description,
            });
          }

          break;
        }
      }

      break;
    }
    case MessageEventDataBodyType.SUBMIT: {
      switch (currentRoute.name) {
        case 'cards': {
          store.commit(
            'utils/cards/needToUpdate',
            CardNeedToUpdateType.USER_TASK
          );

          break;
        }
      }

      break;
    }
    case MessageEventDataBodyType.SUCCESS: {
      Notify.create({
        color: 'positive',
        icon: 'check',
        message: getWebsocketsMsg(data.body.msg),
      });

      break;
    }
    case MessageEventDataBodyType.TASK: {
      if (
        /* Флаг, который настроен для страницы - имеется ли возможность перейти со страницы на степпер */
        get(currentRoute, 'meta.hasRedirectToStepper')
      ) {
        const rootTraceIds = store.getters['wf/processes/waitTraceId'];

        store
          .dispatch('notifierGateway/dispatchUserTask', {
            data,
            rootTraceIds,
          })
          .then(() => {
            if (rootTraceIds.includes(data.traceId)) {
              store.commit('wf/processes/removeWaitTraceId', data.traceId);
            }
          });
      }

      switch (currentRoute.name) {
        case 'cards': {
          store.commit('utils/cards/needToUpdate', CardNeedToUpdateType.TRUE);

          break;
        }
      }

      break;
    }
    case MessageEventDataBodyType.WARNING: {
      Notify.create({
        color: 'warning',
        icon: 'warning',
        message: getWebsocketsMsg(data.body.msg),
      });

      break;
    }
  }
}

/**
 * Функция проверяет - является ли сообщение PING|PONG типом.
 * @param e - Сообщение сокет-канала.
 */
function isPingMessage(e: MessageEvent) {
  const data: any = get(e, 'data');

  if (!data) {
    return false;
  }
  try {
    const body = get(JSON.parse(data), 'body');

    return body && get(body, 'type') === 'PING';
  } catch (e) {
    return false;
  }
}

/**
 * Массив функций, которые будут выполняться по событию onMessage.
 */
let listeners: ((e: MessageEvent) => void)[] = [defaultListener];

function tryReconnect(
  config: WebsocketsConfig
): Promise<{
  instance: WebSocket | null;
}> {
  return new Promise((resolve, reject) => {
    initCookie().then(() => {
      let webSocket: WebSocket | null = null;

      try {
        webSocket = config.protocols
          ? new WebSocket(config.url, config.protocols)
          : new WebSocket(config.url);
      } catch (e) {
        return reject(e);
      }

      if (webSocket) {
        let isBisy = false;

        webSocket.onopen = resetCookie;

        webSocket.onmessage = (e: MessageEvent) => {
          if (isPingMessage(e) && webSocket) {
            webSocket.send(
              JSON.stringify({
                type: 'PONG',
                message: 'Available',
              })
            );
          }
          listeners.forEach((listener: (eMessage: MessageEvent) => void) => {
            listener(e);
          });
        };

        webSocket.onclose = () => {
          if (!isBisy) {
            isBisy = true;

            setTimeout(() => {
              tryReconnect(config).then(
                (webSocketCast: { instance: WebSocket | null }) => {
                  websockets.instance = webSocketCast.instance;
                }
              );
            }, 4000);
          }
        };

        webSocket.onerror = () => {
          if (!isBisy) {
            isBisy = true;

            setTimeout(() => {
              tryReconnect(config).then(
                (webSocketCast: { instance: WebSocket | null }) => {
                  websockets.instance = webSocketCast.instance;
                }
              );
            }, 4000);
          }
        };
      }
      websockets.instance = webSocket;

      return resolve(websockets);
    });
  });
}

export default class Websockets extends Vue {
  /**
   * Геттер для получения instance, не рекоммендуется переопределять onMessage, для этого
   * лучше использовать addListener(Function) и removeListener(Function)
   */
  public get instance(): WebSocket | null {
    return websockets.instance;
  }

  /**
   * Геттер для вывода всех слушателей.
   */
  public get listeners(): ((e: MessageEvent) => void)[] {
    return listeners;
  }

  /**
   * Добавляет функцию прослушки для события onMessage.
   * @param listener{(e: MessageEvent) => void)} - Сама функция для прослушивания пуша.
   */
  public addListener(listener: (e: MessageEvent) => void): void {
    listeners.push(listener);
  }

  /**
   * Удаляет функцию прослушки для события onMessage.
   * @param listener{(e: MessageEvent) => void)} - Передаётся сама функция(сравниваются их ссылки для удаления).
   */
  public removeListener(listener: (e: MessageEvent) => void): void {
    listeners = listeners.filter(
      (itemListener: (e: MessageEvent) => void) => itemListener !== listener
    );
  }

  public static create(
    config: WebsocketsConfig
  ): Promise<{
    instance: WebSocket | null;
  }> {
    return tryReconnect(config);
  }
}

export { WebsocketsConfig, MessageEventData };
