import { AppState, Linking, Platform, AppStateStatus } from 'react-native';
import { UnavailabilityError } from '@unimodules/core';
import ExponentWebBrowser from './ExpoWebBrowser';

import {
  RedirectEvent,
  OpenBrowserOptions,
  AuthSessionResult,
  CustomTabsBrowsersResults,
  BrowserResult,
  RedirectResult,
  MayInitWithUrlResult,
  WarmUpResult,
  CoolDownResult,
} from './WebBrowser.types';

const emptyCustomTabsPackages: CustomTabsBrowsersResults = {
  defaultBrowserPackage: undefined,
  preferredBrowserPackage: undefined,
  browserPackages: [],
  servicePackages: [],
};

export async function getCustomTabsSupportingBrowsersAsync(): Promise<CustomTabsBrowsersResults> {
  if (!ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync) {
    throw new UnavailabilityError('WebBrowser', 'getCustomTabsSupportingBrowsersAsync');
  }
  if (Platform.OS !== 'android') {
    return emptyCustomTabsPackages;
  } else {
    return await ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync();
  }
}

export async function warmUpAsync(browserPackage?: string): Promise<WarmUpResult> {
  if (!ExponentWebBrowser.warmUpAsync) {
    throw new UnavailabilityError('WebBrowser', 'warmUpAsync');
  }
  if (Platform.OS !== 'android') {
    return {};
  } else {
    return await ExponentWebBrowser.warmUpAsync(browserPackage);
  }
}

export async function mayInitWithUrlAsync(
  url: string,
  browserPackage?: string
): Promise<MayInitWithUrlResult> {
  if (!ExponentWebBrowser.mayInitWithUrlAsync) {
    throw new UnavailabilityError('WebBrowser', 'mayInitWithUrlAsync');
  }
  if (Platform.OS !== 'android') {
    return {};
  } else {
    return await ExponentWebBrowser.mayInitWithUrlAsync(url, browserPackage);
  }
}

export async function coolDownAsync(browserPackage?: string): Promise<CoolDownResult> {
  if (!ExponentWebBrowser.coolDownAsync) {
    throw new UnavailabilityError('WebBrowser', 'coolDownAsync');
  }
  if (Platform.OS !== 'android') {
    return {};
  } else {
    return await ExponentWebBrowser.coolDownAsync(browserPackage);
  }
}

export async function openBrowserAsync(
  url: string,
  browserParams: OpenBrowserOptions = {}
): Promise<BrowserResult> {
  if (!ExponentWebBrowser.openBrowserAsync) {
    throw new UnavailabilityError('WebBrowser', 'openBrowserAsync');
  }
  return await ExponentWebBrowser.openBrowserAsync(url, browserParams);
}

export function dismissBrowser(): void {
  if (!ExponentWebBrowser.dismissBrowser) {
    throw new UnavailabilityError('WebBrowser', 'dismissBrowser');
  }
  ExponentWebBrowser.dismissBrowser();
}

export async function openAuthSessionAsync(
  url: string,
  redirectUrl: string
): Promise<AuthSessionResult> {
  if (_authSessionIsNativelySupported()) {
    if (!ExponentWebBrowser.openAuthSessionAsync) {
      throw new UnavailabilityError('WebBrowser', 'openAuthSessionAsync');
    }
    return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl);
  } else {
    return _openAuthSessionPolyfillAsync(url, redirectUrl);
  }
}

export function dismissAuthSession(): void {
  if (_authSessionIsNativelySupported()) {
    if (!ExponentWebBrowser.dismissAuthSession) {
      throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
    }
    ExponentWebBrowser.dismissAuthSession();
  } else {
    if (!ExponentWebBrowser.dismissBrowser) {
      throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
    }
    ExponentWebBrowser.dismissBrowser();
  }
}

/* iOS <= 10 and Android polyfill for SFAuthenticationSession flow */

function _authSessionIsNativelySupported(): boolean {
  if (Platform.OS === 'android') {
    return false;
  }

  const versionNumber = parseInt(String(Platform.Version), 10);
  return versionNumber >= 11;
}

let _redirectHandler: ((event: RedirectEvent) => void) | null = null;

/*
 * openBrowserAsync on Android doesn't wait until closed, so we need to polyfill
 * it with AppState
 */

// Store the `resolve` function from a Promise to fire when the AppState
// returns to active
let _onWebBrowserCloseAndroid: null | (() => void) = null;

function _onAppStateChangeAndroid(state: AppStateStatus) {
  if (state === 'active' && _onWebBrowserCloseAndroid) {
    _onWebBrowserCloseAndroid();
  }
}

async function _openBrowserAndWaitAndroidAsync(startUrl: string): Promise<BrowserResult> {
  let appStateChangedToActive = new Promise(resolve => {
    _onWebBrowserCloseAndroid = resolve;
    AppState.addEventListener('change', _onAppStateChangeAndroid);
  });

  let result: BrowserResult = { type: 'cancel' };
  let { type } = await openBrowserAsync(startUrl);

  if (type === 'opened') {
    await appStateChangedToActive;
    result = { type: 'dismiss' };
  }

  AppState.removeEventListener('change', _onAppStateChangeAndroid);
  _onWebBrowserCloseAndroid = null;
  return result;
}

async function _openAuthSessionPolyfillAsync(
  startUrl: string,
  returnUrl: string
): Promise<AuthSessionResult> {
  if (_redirectHandler) {
    throw new Error(
      `The WebBrowser's auth session is in an invalid state with a redirect handler set when it should not be`
    );
  }

  if (_onWebBrowserCloseAndroid) {
    throw new Error(`WebBrowser is already open, only one can be open at a time`);
  }

  try {
    if (Platform.OS === 'android') {
      return await Promise.race([
        _openBrowserAndWaitAndroidAsync(startUrl),
        _waitForRedirectAsync(returnUrl),
      ]);
    } else {
      return await Promise.race([openBrowserAsync(startUrl), _waitForRedirectAsync(returnUrl)]);
    }
  } finally {
    // We can't dismiss the browser on Android, only call this when it's available.
    // Users on Android need to manually press the 'x' button in Chrome Custom Tabs, sadly.
    if (ExponentWebBrowser.dismissBrowser) {
      ExponentWebBrowser.dismissBrowser();
    }

    _stopWaitingForRedirect();
  }
}

function _stopWaitingForRedirect() {
  if (!_redirectHandler) {
    throw new Error(
      `The WebBrowser auth session is in an invalid state with no redirect handler when one should be set`
    );
  }

  Linking.removeEventListener('url', _redirectHandler);
  _redirectHandler = null;
}

function _waitForRedirectAsync(returnUrl: string): Promise<RedirectResult> {
  return new Promise(resolve => {
    _redirectHandler = (event: RedirectEvent) => {
      if (event.url.startsWith(returnUrl)) {
        resolve({ url: event.url, type: 'success' });
      }
    };

    Linking.addEventListener('url', _redirectHandler);
  });
}