import 'abort-controller/polyfill';
import { UnavailabilityError } from 'expo-modules-core';

import ServerRegistrationModule from './ServerRegistrationModule';
import { addPushTokenListener } from './TokenEmitter';
import { DevicePushToken } from './Tokens.types';
import { getDevicePushTokenAsync } from './getDevicePushTokenAsync';
import {
  updateDevicePushTokenAsync as updateDevicePushTokenAsyncWithSignal,
  hasDeviceTokenChangedAsync,
} from './utils/updateDevicePushTokenAsync';

let lastAbortController: AbortController | null = null;
async function updatePushTokenAsync(token: DevicePushToken) {
  const changed = await hasDeviceTokenChangedAsync(token);
  if (!changed) {
    return;
  }

  // Abort current update process
  lastAbortController?.abort();
  lastAbortController = new AbortController();
  return await updateDevicePushTokenAsyncWithSignal(lastAbortController.signal, token);
}

/**
 * Encapsulates device server registration data
 */
export type DevicePushTokenRegistration = {
  isEnabled: boolean;
};

/**
 * @hidden - the comment is misleading and the purpose of the function needs to be reevaluated
 *
 * Sets the registration information so that the device push token gets pushed
 * to the given registration endpoint
 * @param enabled
 */
export async function setAutoServerRegistrationEnabledAsync(enabled: boolean) {
  // We are overwriting registration, so we shouldn't let
  // any pending request complete.
  lastAbortController?.abort();

  if (!ServerRegistrationModule.setRegistrationInfoAsync) {
    throw new UnavailabilityError('ServerRegistrationModule', 'setRegistrationInfoAsync');
  }

  if (!enabled) {
    await ServerRegistrationModule.setRegistrationInfoAsync(null);
  } else {
    let existing: Record<string, unknown> = {};
    try {
      const info = await ServerRegistrationModule.getRegistrationInfoAsync?.();
      if (info) {
        existing = JSON.parse(info);
      }
    } catch {}
    existing.isEnabled = true;
    await ServerRegistrationModule.setRegistrationInfoAsync(JSON.stringify(existing));
  }
}

// note(Chmiela): This function is exported only for testing purposes.
export async function __handlePersistedRegistrationInfoAsync(
  registrationInfo: string | null | undefined
) {
  if (!registrationInfo) {
    // No registration info, nothing to do
    return;
  }

  let registration: DevicePushTokenRegistration | null = null;
  try {
    registration = JSON.parse(registrationInfo);
  } catch (e) {
    console.warn(
      '[expo-notifications] Error encountered while fetching registration information for auto token updates.',
      e
    );
  }

  if (!registration?.isEnabled) {
    // Registration is invalid or not enabled, nothing more to do
    return;
  }

  try {
    // Since the registration is enabled, fetching a "new" device token
    // shouldn't be a problem.
    const latestDevicePushToken = await getDevicePushTokenAsync();
    await updatePushTokenAsync(latestDevicePushToken);
  } catch (e) {
    console.warn(
      '[expo-notifications] Error encountered while updating server registration with latest device push token.',
      e
    );
  }
}

if (ServerRegistrationModule.getRegistrationInfoAsync) {
  // A global scope (to get all the updates) device push token
  // subscription, never cleared.
  addPushTokenListener(async (token) => {
    try {
      // Before updating the push token on server we always check if we should
      // Since modules can't change their method availability while running, we
      // can assert it's defined.
      const registrationInfo = await ServerRegistrationModule.getRegistrationInfoAsync!();

      if (!registrationInfo) {
        // Registration is not enabled
        return;
      }

      const registration: DevicePushTokenRegistration | null = JSON.parse(registrationInfo);
      if (registration?.isEnabled) {
        // Dispatch an abortable task to update
        // registration with new token.
        await updatePushTokenAsync(token);
      }
    } catch (e) {
      console.warn(
        '[expo-notifications] Error encountered while updating server registration with latest device push token.',
        e
      );
    }
  });

  // Verify if persisted registration
  // has successfully uploaded last known
  // device push token. If not, retry.
  ServerRegistrationModule.getRegistrationInfoAsync().then(
    __handlePersistedRegistrationInfoAsync,
    (e) => {
      console.error('[expo-notifications] Error reading persisted server registration info: ', e);
    }
  );
} else {
  console.warn(
    `[expo-notifications] Error encountered while fetching auto-registration state, new tokens will not be automatically registered on server.`,
    new UnavailabilityError('ServerRegistrationModule', 'getRegistrationInfoAsync')
  );
}
