import { EmitterSubscription, NativeEventEmitter } from 'react-native';
import { throwOnInvliadResponse } from './http';

export interface WifiConnectOptions {
  /**
   * An access token that allows access to the WiFi Connect API for the current user.
   */
  accessToken: string;

  /**
   * Usually, network and WiFi support is limited running on a simulator. Therefore,
   * the WiFi configuration can not be stored on a simulated device. In such a case
   * `WifiConnect.connectToWifi()` will reject with the error code `E_NOT_SUPPORTED_ON_SIMULATOR`.
   * If you don't want to reject with this error but instead resolve successfully, set this
   * value to `true`. Defaults to `false`.
   */
  ignoreNetworkErrorOnSimulator?: boolean;

  /**
   * Optionally set a different REST API endpoint that should be used by this library.
   */
  wifiApiEndpoint?: string;

  /**
   * Optional Android specific options.
   */
  android?: {
    /**
     * This value is only used for `connectToWifi` on Android 10 devices.
     *
     * This time span is used to wait for granting/declining the permission dialog which
     * is shown by the Operating System. If the user declines the permission dialog within
     * the time span, `connectToWifi` will reject with the corresponding error code.
     * If the user grants the permissions or the dialog is still opened, `connectToWifi` will
     * resolve after the time span.
     *
     * This value defaults to 10 seconds if not specified.
     */
    timeSpanToWaitForPermissionDialogConfirmationInSeconds?: number;
  };

  /**
   * Specifies whether connection represents a hidden network.
   *
   * This value defaults to false if not specified.
   */
  isHiddenSSID?: boolean;
}

export interface LegalTerms {
  /**
   * Legal terms content
   */
  legalTerms: string;

  /**
   * Version of legal terms.
   */

  version: string;
  /**
   * Minimum legal terms version which must be accepted to access the API.
   */
  minimumVersion: string;

  /**
   * The date at which the minimal version of legal terms will be enforced.
   */
  dateMinLegalTermsActive: string;
}

export interface User {
  /**
   * The e-mail address of the user.
   */
  email: string;

  /**
   * The preferred locale that should be used to interact with the user.
   */
  preferredLocale: string;
}

export interface WifiConnectService {
  /**
   * Register this device to get direct access to abl's WiFi networks. The device will be configured
   * to use this WiFi connection. This operation is an asynchrnous process and might take a few seconds
   * to complete.
   * @param deviceId A string that uniquely identifies this device.
   * @param user Details about the user that owns the device.
   * @returns A void promise that resolves if the devices registration was successful and the WiFi
   * configuration is stored in the device settings. Rejects if configuring the WiFi network failed
   * for any reason.
   */
  connectToWifi(deviceId: string, user: User): Promise<void>;

  /**
   * Gets the latest legal terms that must be accepted by the end-user to use the WiFi network.
   * @returns A promise that resolves the legal terms or rejects if loading the legal terms failed.
   */
  getLatestLegalTerms(): Promise<LegalTerms>;

  /**
   * Checks whether the specified user already accepted the legal terms or not.
   * @returns `true` if the user already accepted the legal terms, otherwise false.
   */
  legalTermsAccepted(): Promise<boolean>;

  /**
   * Checks the version of legal terms accepted by the user.
   * @returns version of the legal terms that was accepted by the user. If the user didn't accepted any legal terms yet - returns 'undefined'
   */
  legalTermsAcceptedVersion(): Promise<string>;

  /**
   * Accept the specified version of legal terms. This method needs to be called before a user
   * can register to connect to a WiFi network.
   * @param legalTermsVersion The version of the legal terms.
   */
  acceptLegalTerms(legalTermsVersion: string): Promise<void>;

  /**
   * Deletes the WiFi settings from the device and unregisters the device
   * from abl servers.
   * Basically, it just reverts the changes that were made by `connectToWifi()`.
   * @param deviceId A string that uniquely identifies this device.
   */
  deleteWifiConfiguration(deviceId: string): Promise<void>;

  /**
   * Check if the device is configured to connect to abl's WiFi network.
   * @returns `true` if the device is configured; otherwise `false`.
   */
  isWifiConfigured(): Promise<boolean>;

  /**
   * Check if device is connected to abl's WiFi network.
   * Method should be invoked after connectToWifi() to check connection by received SSID, othervise resolves with `false`.
   * @returns `true` if the device is connected; otherwise `false`.
   */
  isConnectedToWifi(): Promise<boolean>;

  /**
   * Registers a callback that will be invoked if the required permissions are
   * revoked after `connectToWifi` fullfilled. This callback can be used to handle
   * the case if a user rejects the permission dialog after the confirmation time span.
   *
   * This callback should be unregistered (using `unregisterOnPermissionRejectedListener`)
   * after usage to free internal resources.
   *
   * This callback will only be invoked on Android 10 devices.
   * @param callback The callback to invoke.
   */
  registerOnPermissionRejectedListener(callback: () => void): void;

  /**
   * Unregister the callback that was previously registered using
   * `registerOnPermissionRejectedListener`.
   */
  unregisterOnPermissionRejectedListener(): void;
}

export interface NativeWifiConnect {
  connectToWifi(args: ConnectToWifiArgs): Promise<void>;
  deleteConfiguration(args: DeleteConfigurationArgs): Promise<void>;
  isWifiConfigured(): Promise<boolean>;
  isConnectedToWifi(ssid: string): Promise<boolean>;
}

export class WifiConnectServiceImpl implements WifiConnectService {
  private readonly options: Required<WifiConnectOptions>;
  private legalTerms: LegalTerms | null = null;
  private permissionRejectedListener: EmitterSubscription | null = null;
  private SSID: string | null = null;

  constructor(
    private readonly nativeWifiConnect: NativeWifiConnect,
    wifiConnectionOptions: WifiConnectOptions
  ) {
    this.throwIfInvalidOptions(wifiConnectionOptions);
    this.options = this.mergeWithDefaultOptions(wifiConnectionOptions);
  }

  public async connectToWifi(deviceId: string, user: User): Promise<void> {
    const endpoint = `${this.options.wifiApiEndpoint}/api/v1/devices/${deviceId}`;

    var response = await fetch(endpoint, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${this.options.accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: user.email,
        preferredLocale: user.preferredLocale,
      }),
    });

    const body: CreateDeviceResponse = await response.json();

    throwOnInvliadResponse(response.status, body);

    this.SSID = body.wifiCredentials.authorizedNetwork.ssid;

    const args: ConnectToWifiArgs = {
      caCertificate: body.wifiCredentials.authorizedNetwork.caCertificate,
      domainSuffix: body.wifiCredentials.authorizedNetwork.wpa2Domain,
      ignoreNetworkErrorOnSimulator: this.options.ignoreNetworkErrorOnSimulator,
      password: body.wifiCredentials.password,
      ssid: body.wifiCredentials.authorizedNetwork.ssid,
      username: body.wifiCredentials.username,
      timeSpanToWaitForPermissionDialogConfirmationInSeconds:
        this.options.android
          .timeSpanToWaitForPermissionDialogConfirmationInSeconds ?? 10,
      isHiddenSSID: this.options.isHiddenSSID,
    };
    await this.nativeWifiConnect.connectToWifi(args);
  }

  public async getLatestLegalTerms(): Promise<LegalTerms> {
    if (this.legalTerms) {
      return this.legalTerms;
    }

    const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms`;

    var response = await fetch(endpoint, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${this.options.accessToken}`,
      },
    });

    if (response.status !== 200) {
      throw new Error(
        `Could not load legal terms. Received status ${response.status} from API.`
      );
    }
    const body: LegalTerms = await response.json();

    this.legalTerms = body;

    return body;
  }

  public async legalTermsAccepted(): Promise<boolean> {
    const legalTerms = await this.getLatestLegalTerms();

    const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms/acceptedVersion`;

    var response = await fetch(endpoint, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${this.options.accessToken}`,
      },
    });

    // The API will return a 404 if the current user did not accept any legal terms version at all.
    if (response.status === 404) {
      return false;
    }

    if (response.status !== 200) {
      throw new Error(
        `Could not load the currently accepted version. Received status ${response.status} from API.`
      );
    }
    const body: AcceptedLegalTermsResponse = await response.json();

    return body.version === legalTerms.version;
  }

  public async legalTermsAcceptedVersion(): Promise<string> {
    const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms/acceptedVersion`;

    var response = await fetch(endpoint, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${this.options.accessToken}`,
      },
    });

    // The API will return a 404 if the current user did not accept any legal terms version at all.
    if (response.status === 404) {
      return 'undefined';
    }

    if (response.status !== 200) {
      throw new Error(
        `Could not load the currently accepted version. Received status ${response.status} from API.`
      );
    }
    const body: AcceptedLegalTermsResponse = await response.json();

    return body.version;
  }

  public async acceptLegalTerms(legalTermsVersion: string): Promise<void> {
    const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms/${legalTermsVersion}/accept`;

    var response = await fetch(endpoint, {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${this.options.accessToken}`,
      },
    });

    if (response.status !== 200 && response.status !== 202) {
      throw new Error(
        `Accepting the legal terms failed. Received status ${response.status} from API.`
      );
    }
  }

  public async deleteWifiConfiguration(deviceId: string): Promise<void> {
    const endpoint = `${this.options.wifiApiEndpoint}/api/v1/devices/${deviceId}`;

    var response = await fetch(endpoint, {
      method: 'DELETE',
      headers: {
        Authorization: `Bearer ${this.options.accessToken}`,
      },
    });

    throwOnInvliadResponse(response.status, null, true);

    const args: DeleteConfigurationArgs = {
      ignoreNetworkErrorOnSimulator: this.options.ignoreNetworkErrorOnSimulator,
    };
    await this.nativeWifiConnect.deleteConfiguration(args);
  }

  public async isWifiConfigured(): Promise<boolean> {
    return await this.nativeWifiConnect.isWifiConfigured();
  }

  public registerOnPermissionRejectedListener(callback: () => void): void {
    const eventEmitter = new NativeEventEmitter(this.nativeWifiConnect as any);
    this.permissionRejectedListener = eventEmitter.addListener(
      'PermissionRejected',
      () => callback()
    );
  }

  public unregisterOnPermissionRejectedListener(): void {
    const listener = this.permissionRejectedListener;
    if (listener) {
      listener.remove();
      this.permissionRejectedListener = null;
    }
  }

  public isConnectedToWifi = (): Promise<boolean> =>
    this.SSID
      ? this.nativeWifiConnect.isConnectedToWifi(this.SSID)
      : Promise.resolve(false);

  private mergeWithDefaultOptions(
    options: WifiConnectOptions
  ): Required<WifiConnectOptions> {
    return {
      accessToken: options.accessToken,
      ignoreNetworkErrorOnSimulator:
        options.ignoreNetworkErrorOnSimulator ?? false,
      wifiApiEndpoint:
        options?.wifiApiEndpoint ||
        'https://api.wifi.connectivity.abl-solutions.io',
      android: options.android ?? {},
      isHiddenSSID: options?.isHiddenSSID || false,
    };
  }

  private throwIfInvalidOptions(options: WifiConnectOptions) {
    if (!options) {
      throw new Error('WifiConnectOptions must be not null.');
    }

    if (!options.accessToken) {
      throw new Error('WifiConnectOptions.accessToken must be not null.');
    }

    if (options.accessToken === '') {
      throw new Error('WifiConnectOptions.accessToken must be not empty.');
    }
  }
}

interface AcceptedLegalTermsResponse {
  version: string;
}

interface CreateDeviceResponse {
  deviceId: string;
  wifiCredentials: {
    username: string;
    password: string;
    authorizedNetwork: {
      caCertificate: string;
      ssid: string;
      wpa2Domain: string;
    };
  };
}

interface ConnectToWifiArgs extends CommonNativeArgs {
  caCertificate: string;
  domainSuffix: string;
  password: string;
  ssid: string;
  username: string;
  timeSpanToWaitForPermissionDialogConfirmationInSeconds: number;
  isHiddenSSID: boolean;
}

interface DeleteConfigurationArgs extends CommonNativeArgs {}

interface CommonNativeArgs {
  ignoreNetworkErrorOnSimulator: boolean;
}
