import { logger } from 'tachyon-logger';
import badgerService, { BadgerService } from 'mweb/chat/badgerService';
import chatClient, {
  ChatClient,
  ChatHandler,
  ModerationHandler,
  UserStatePayload,
  SubscriptionMethods,
} from 'mweb/chat/chatClient';
import {
  POST,
  ACTION,
  MessageEventType,
  messageEvent,
} from 'mweb/chat/events/messageEvent';
import {
  BAN,
  TIMEOUT,
  moderationEvent,
  ModerationEventType,
} from 'mweb/chat/events/moderationEvent';
import {
  subscriptionEvent,
  resubscriptionEvent,
} from 'mweb/chat/events/subscribeEvent';
import {
  connectedEvent,
  disconnectedEvent,
  reconnectEvent,
  hostingEvent,
  unhostEvent,
} from 'mweb/chat/events/statusEvent';
import { ChatEvent } from 'mweb/chat/events/baseChatEvent';

export const CHAT_WORKER_CONNECTED = 'CHAT_WORKER_CONNECTED';
interface ConnectMessage {
  command: typeof CHAT_WORKER_CONNECTED;
  payload: {
    channelName: string;
    channelID: string;
    polyfillURI: string | null;
  };
}

const CHAT_WORKER_DISCONNECTED = 'CHAT_WORKER_DISCONNECTED';
interface DisconnectMessage {
  command: typeof CHAT_WORKER_DISCONNECTED;
}

export const CHAT_WORKER_CHANNEL_CHANGED = 'CHAT_WORKER_CHANNEL_CHANGED';
interface ChangeChannelMessage {
  command: typeof CHAT_WORKER_CHANNEL_CHANGED;
  payload: {
    channelName: string;
    channelID: string;
  };
}

export type WorkerMessage =
  | ConnectMessage
  | DisconnectMessage
  | ChangeChannelMessage;

export class ChatService {
  channelName: string;
  channelID: string;

  constructor(
    public importScripts: (...targets: string[]) => void,
    public postMessage: (
      message: ChatEvent,
      targetOrigin?: string,
      transfer?: any[],
    ) => void,
    public client: ChatClient = chatClient,
    public badger: BadgerService = badgerService,
  ) {}

  messageHandler(message: {
    data: WorkerMessage;
  }): Promise<[void, void] | void> {
    switch (message.data.command) {
      case CHAT_WORKER_CONNECTED:
        if (!message.data.payload.polyfillURI) {
          logger.error('No polyfill URI provided in chat service');
        } else {
          try {
            logger.info([
              'Polyfilling worker:',
              message.data.payload.polyfillURI,
            ]);
            this.importScripts(message.data.payload.polyfillURI);
          } catch (e) {
            // log error but continue, giving newer browsers a chance since all we need is fetch
            logger.error({
              message: 'Error fetching polyfill in chat service',
              error: e,
            });
          }
        }
        this.channelName = message.data.payload.channelName;
        this.channelID = message.data.payload.channelID;
        return this.connect();
      case CHAT_WORKER_DISCONNECTED:
        return this.disconnect();
      case CHAT_WORKER_CHANNEL_CHANGED:
        this.channelName = message.data.payload.channelName;
        this.channelID = message.data.payload.channelID;
        return this.changeChannel();
      default:
        logger.error({
          message: 'Unknown message type:',
          messageData: message.data,
        });
        return Promise.resolve(undefined);
    }
  }

  async connect(): Promise<[void, void] | void> {
    logger.info(`Instantiating client and connecting to ${this.channelName}`);

    this.client.setConnectedHandler(this.onConnectedEvent);
    this.client.setDisconnectedHandler(this.onDisconnectedEvent);
    this.client.setReconnectHandler(this.onReconnectEvent);

    this.client.setHostingHandler(this.onHostingEvent);
    this.client.setUnhostHandler(this.onUnhostEvent);

    this.client.setChatHandler(this.onChatEvent(POST));
    this.client.setActionHandler(this.onChatEvent(ACTION));
    this.client.setTimeoutHandler(this.onModerationEvent(TIMEOUT));
    this.client.setBanHandler(this.onModerationEvent(BAN));
    this.client.setSubcriptionHandler(this.onSubscriptionEvent);
    this.client.setResubscriptionHandler(this.onResubscriptionEvent);

    try {
      await Promise.all([
        this.client.connect(this.channelName),
        this.badger.init(this.channelID),
      ]);
    } catch (e) {
      logger.error({
        message: 'Error with initial connection to chat service',
        error: e,
      });
      if (!this.client.isSocketOpen()) {
        this.postMessage(reconnectEvent());
      }
    }
  }

  async disconnect(): Promise<void> {
    logger.info(`Disconnecting client and closing worker`);
    await this.client.disconnect();
    close();
  }

  async changeChannel(): Promise<void> {
    logger.info(`Changing channel connection to ${this.channelName}`);
    try {
      await Promise.all([
        this.client.changeChannel(this.channelName),
        this.badger.init(this.channelID),
      ]);
      this.onChannelChangeEvent();
    } catch (e) {
      logger.error({
        message: 'Error with changing chat channel',
        error: e,
      });
    }
  }

  onConnectedEvent = (_address: string, _port: number) =>
    this.postMessage(connectedEvent());

  onChannelChangeEvent = () => this.postMessage(connectedEvent());

  onDisconnectedEvent = (reason: string) =>
    this.postMessage(disconnectedEvent(reason));

  onReconnectEvent = () => this.postMessage(reconnectEvent());

  onHostingEvent =
    // TMI.js formats channel names like `#monstercat`
    (channel: string, target: string, _viewers: number) =>
      this.postMessage(hostingEvent(channel.slice(1), target));

  onUnhostEvent =
    // TMI.js formats channel names like `#monstercat`
    (channel: string, _viewers: number) =>
      this.postMessage(unhostEvent(channel.slice(1)));

  onChatEvent = (type: MessageEventType): ChatHandler => (
    _channel: string,
    userstate: UserStatePayload,
    message: string,
    _sentByCurrentUser: boolean,
  ): void =>
    this.postMessage(
      messageEvent(
        type,
        message,
        userstate,
        this.badger.getBadgeData(userstate.badges),
      ),
    );

  onModerationEvent = (type: ModerationEventType): ModerationHandler => (
    _channel: string,
    username: string,
    reason: string,
    duration?: number,
  ): void =>
    this.postMessage(moderationEvent(type, username, reason, duration));

  onSubscriptionEvent =
    // TMI.js formats channel names like `#monstercat`
    (channel: string, username: string, methods: SubscriptionMethods): void =>
      this.postMessage(
        subscriptionEvent(channel.slice(1), username, methods.prime),
      );

  onResubscriptionEvent =
    // TMI.js formats channel names like `#monstercat`
    (
      channel: string,
      username: string,
      months: number,
      message: string,
      userstate: UserStatePayload,
      methods: SubscriptionMethods,
    ): void =>
      this.postMessage(
        resubscriptionEvent(
          channel.slice(1),
          username,
          methods.prime,
          months,
          message,
          userstate,
          userstate ? this.badger.getBadgeData(userstate.badges) : [],
        ),
      );
}
