import jwtDecode from 'jwt-decode';
/* eslint-disable no-param-reassign */
import { modifyMqttConnection } from '~/client/utils/modifyMqttConnection';
/* eslint-disable require-atomic-updates */
import { proxyWebsocketEvents } from '~/core/events';
import { onChannelDeleted } from '~/channelRepository/events/onChannelDeleted';
import { onChannelMemberBanned } from '~/channelRepository/events/onChannelMemberBanned';

import { markReadEngineOnLoginHandler } from '~/subChannelRepository/utils/markReadEngine';
import { onUserDeleted } from '~/userRepository/events/onUserDeleted';

import analyticsEngineOnLoginHandler from '~/analytic/utils/analyticsEngineOnLoginHandler';
import readReceiptSyncEngineOnLoginHandler from '~/client/utils/ReadReceiptSync/readReceiptSyncEngineOnLoginHandler';
import legacyReadReceiptSyncEngineOnLoginHandler from '~/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngineOnLoginHandler';
import objectResolverEngineOnLoginHandler from '~/client/utils/ObjectResolver/objectResolverEngineOnLoginHandler';
import { logout } from './logout';

import { getActiveClient } from './activeClient';
import { terminateClient } from './terminateClient';
import { setActiveUser } from './activeUser';
import { setSessionState } from './setSessionState';

import { onClientBanned } from '../events';
import { onTokenExpired } from '../events/onTokenExpired';
import { onTokenTerminated } from '../events/onTokenTerminated';

import { removeChannelMarkerCache } from '../utils/removeChannelMarkerCache';
import { initializeMessagePreviewSetting } from '../utils/messagePreviewEngine';
import { ASCError } from '~/core/errors';
import SessionWatcher from '../utils/SessionWatcher';

/*
 * declared earlier to accomodate case when logging in with a different user
 * than the one already connected, in which case the existing subscriptions need
 * to be cleared
 */
let subscriptions: Amity.Unsubscriber[] = [];

async function runMqtt() {
  await modifyMqttConnection();
}

interface Params {
  userId: string;
  token: {
    issuedAt: string;
    expiresAt: string;
    accessToken: string;
  };
}

const isSameUserId = (token: Params['token']['accessToken']) => {
  const client = getActiveClient();

  const decoded = jwtDecode<{
    user: {
      userId: string;
      publicUserId: string;
      deviceInfo: Amity.Device['deviceInfo'];
      networkId: string;
      displayName: string;
      refreshToken: string;
    };
  }>(token);

  return decoded?.user?.publicUserId === client.userId;
};

const validateAccessToken = async ({ token, userId }: Params) => {
  const client = getActiveClient();
  // begin establishing session
  setSessionState(Amity.SessionStates.ESTABLISHING);

  const {
    data: { users },
  } = await client.http.get(`/api/v3/users/${userId}`, {
    headers: {
      Authorization: `Bearer ${token.accessToken}`,
    },
  });

  const user = users.find((u: Amity.User) => u.userId === userId);

  client.http.defaults.headers.common.Authorization = `Bearer ${token.accessToken}`;

  client.http.defaults.metadata = {
    tokenExpiry: token.expiresAt,
    isGlobalBanned: false,
    isUserDeleted: false,
  };

  client.upload.defaults.headers.common.Authorization = `Bearer ${token.accessToken}`;

  client.upload.defaults.metadata = {
    tokenExpiry: token.expiresAt,
    isGlobalBanned: false,
    isUserDeleted: false,
  };

  // manually setup the token for ws transport
  if (client.ws) client.ws.io.opts.query = { token: token.accessToken };

  client.token = token;

  setSessionState(Amity.SessionStates.ESTABLISHED);

  return user;
};

/* begin_public_function
  id: client.resumeSession
*/
/**
 * ```js
 * import { resumeSession } from '@amityco/ts-sdk/client/api'
 * const success = await resumeSession({
 *   userId: 'XYZ123456789',
 *   token: { accessToken: 'abc123', issuedAt: '2023-01-01T00:00:00Z', expiresAt: '2023-01-02T00:00:00Z' }
 * })
 * ```
 *
 * Connects an {@link Amity.Client} instance to ASC servers using an existing access token
 *
 * @param params the connect parameters
 * @param params.userId the user ID for the current session
 * @param params.token the existing access token with its metadata
 * @param sessionHandler the session handler for token renewal
 * @param config optional configuration
 * @returns a success boolean if connected
 *
 * @category Client API
 * @async
 */
export const resumeSession = async (
  params: Params,
  sessionHandler: Amity.SessionHandler,
  config?: Amity.ConnectClientConfig,
): Promise<boolean> => {
  const client = getActiveClient();
  let unsubWatcher: Amity.Unsubscriber;

  client.log('client/api/resumeSession', {
    apiKey: client.apiKey,
    sessionState: client.sessionState,
    ...params,
  });

  // Handle existing connected user
  if (client.userId) {
    if (client.userId === params.userId && isSameUserId(params.token.accessToken)) {
      // Clear connections and listeners but preserve cache
      if (client.mqtt && client.mqtt.connected) {
        client.mqtt.disconnect();
      }
      if (client.ws && client.ws.connected) {
        client.ws.disconnect();
      }

      // Clear existing subscriptions
      subscriptions.forEach(fn => fn());
      subscriptions = [];
    } else {
      // Different user - do full logout
      await logout();

      // Remove subscription to ban and delete
      subscriptions.forEach(fn => fn());
      subscriptions = [];
    }
  }

  try {
    const user = await validateAccessToken(params);

    if (user == null) {
      throw new ASCError(
        `${params.userId} has not been found`,
        Amity.ClientError.UNKNOWN_ERROR,
        Amity.ErrorLevel.ERROR,
      );
    }

    if (user.isDeleted) {
      terminateClient(Amity.TokenTerminationReason.USER_DELETED);
      return false;
    }

    if (user.isGlobalBanned) {
      terminateClient(Amity.TokenTerminationReason.GLOBAL_BAN);
      return false;
    }

    // FIXME: events are duplicated if connectClient is called few times without disconnectClient
    // wire websocket events to our event emitter
    proxyWebsocketEvents(client.ws, client.emitter);

    client.ws?.open();

    client.userId = user.userId;

    client.sessionHandler = sessionHandler;

    /*
     * Cannot push to subscriptions as watcher needs to continue working even if
     * token expires
     */
    unsubWatcher = client.accessTokenExpiryWatcher(sessionHandler);

    setActiveUser(user);
  } catch (error) {
    /*
     * if getting token failed session state reverts to initial state when app
     * is first launched
     */
    SessionWatcher.getInstance().setSessionState(Amity.SessionStates.NOT_LOGGED_IN);

    // pass error down tree so the calling function handle it
    throw error;
  }

  if (config?.disableRTE !== true) {
    runMqtt();
  }

  await initializeMessagePreviewSetting();

  if (subscriptions.length === 0) {
    subscriptions.push(
      // GLOBAL_BAN
      onClientBanned((_: Amity.UserPayload) => {
        terminateClient(Amity.TokenTerminationReason.GLOBAL_BAN);

        subscriptions.forEach(fn => fn());

        unsubWatcher();
      }),

      onTokenTerminated(_ => {
        terminateClient();

        subscriptions.forEach(fn => fn());

        unsubWatcher();
      }),

      onUserDeleted((user: Amity.InternalUser) => {
        if (user.userId === client.userId) {
          terminateClient(Amity.TokenTerminationReason.USER_DELETED);

          subscriptions.forEach(fn => fn());

          unsubWatcher();
        }
      }),

      onTokenExpired(state => {
        SessionWatcher.getInstance().setSessionState(state);

        logout();

        subscriptions.forEach(fn => fn());
      }),

      // NOTE: This is a temporary solution to handle the channel marker when the user is forced to leave
      // the channel because currently backend can't handle this, so every time a user is banned from
      // a channel or the channel is deleted the channel's unread count will not be reset to zero
      onChannelDeleted(removeChannelMarkerCache),
      onChannelMemberBanned(removeChannelMarkerCache),

      markReadEngineOnLoginHandler(),
      analyticsEngineOnLoginHandler(),
      objectResolverEngineOnLoginHandler(),
    );

    if (client.useLegacyUnreadCount) {
      subscriptions.push(readReceiptSyncEngineOnLoginHandler());
    } else subscriptions.push(legacyReadReceiptSyncEngineOnLoginHandler());
  }

  return true;
};
/* end_public_function */
