import { Observable } from 'rxjs';

import {
  NativeModules,
  NativeEventEmitter,
  DeviceEventEmitter,
  Platform,
} from 'react-native';

import {
  StripeTerminalConfig,
  DiscoveryMethod,
  Reader,
  ReaderSoftwareUpdate,
  ConnectionStatus,
  ReaderInputOptions,
  ReaderDisplayMessage,
  PaymentIntent,
} from './utils';
const { MadStripeSdk } = NativeModules;

/**
 * The Android connection status enum is different from iOS, this maps Android to iOS
 * @ignore
 */
const AndroidConnectionStatusMap = {
  0: ConnectionStatus.NotConnected,
  1: ConnectionStatus.Connecting,
  2: ConnectionStatus.Connected,
};

export class StripeTerminalSDK {
  private _fetchConnectionToken: () => Promise<string> = () =>
    Promise.reject('You must initialize StripeTerminalSDK first.');
  private _onUnexpectedReaderDisconnect: () => void = () =>
    Promise.reject('You must initialize StripeTerminalSDK first.');
  static listener: NativeEventEmitter;
  public isInitialized = false;
  private static isDiscovering: boolean = false;

  constructor(options: StripeTerminalConfig) {
    StripeTerminalSDK.listener =
      Platform.OS === 'ios'
        ? new NativeEventEmitter(NativeModules.ReactNativeEventEmitter)
        : DeviceEventEmitter;
    this._fetchConnectionToken = options.fetchConnectionToken;
  }

  private async init() {
    StripeTerminalSDK.listener.addListener('requestConnectionToken', () => {
      this._fetchConnectionToken()
        .then((token) => {
          if (token) {
            console.log(token);
            MadStripeSdk.setConnectionToken(token, null);
          } else {
            throw new Error(
              'User-supplied `fetchConnectionToken` resolved successfully, but no token was returned.'
            );
          }
        })
        .catch((err) =>
          MadStripeSdk.setConnectionToken(
            null,
            err.message || 'Error in user-supplied `fetchConnectionToken`.'
          )
        );
    });

    StripeTerminalSDK.listener.addListener(
      'didReportUnexpectedReaderDisconnect',
      () => {
        this._onUnexpectedReaderDisconnect();
      }
    );

    await MadStripeSdk.initTerminal();

    this.isInitialized = true;
  }

  private _listenerToObservable(
    name:
      | 'didRequestReaderDisplayMessage'
      | 'didRequestReaderInput'
      | 'didReportAvailableUpdate'
      | 'didStartInstallingUpdate'
      | 'didReportReaderSoftwareUpdateProgress'
      | 'didFinishInstallingUpdate',
    transformFunc?: (data: any) => any
  ): Observable<any> {
    return new Observable((subscriber) => {
      StripeTerminalSDK.listener.addListener(name, (data: any) => {
        if (transformFunc) {
          return subscriber.next(transformFunc(data));
        }

        return subscriber.next(data);
      });

      return {
        unsubscribe: () => {
          StripeTerminalSDK.listener.removeListener(name, () => {});
        },
      };
    });
  }

  public didStartInstallingUpdate(): Observable<ReaderSoftwareUpdate> {
    return this._listenerToObservable(
      'didStartInstallingUpdate',
      (data: { update: ReaderSoftwareUpdate }) => {
        return data?.update ?? null;
      }
    );
  }

  public didReportAvailableUpdate(): Observable<ReaderSoftwareUpdate> {
    return this._listenerToObservable(
      'didReportAvailableUpdate',
      (data: { update: ReaderSoftwareUpdate }) => {
        return data?.update ?? null;
      }
    );
  }

  public didReportReaderSoftwareUpdateProgress(): Observable<number> {
    return this._listenerToObservable(
      'didReportReaderSoftwareUpdateProgress',
      (data: any) => {
        return parseFloat(data.progress);
      }
    );
  }

  public didFinishInstallingUpdate(): Observable<{
    update?: ReaderSoftwareUpdate;
    error?: string;
  }> {
    return this._listenerToObservable(
      'didFinishInstallingUpdate',
      (data: { update?: ReaderSoftwareUpdate; error?: string }) => {
        return data ?? null;
      }
    );
  }

  public didRequestReaderInput(): Observable<ReaderInputOptions> {
    return this._listenerToObservable('didRequestReaderInput', (data: any) => {
      if (data.isAndroid) {
        return data.value;
      }

      return parseFloat(data.value);
    });
  }

  public didRequestReaderDisplayMessage(): Observable<ReaderDisplayMessage> {
    return this._listenerToObservable(
      'didRequestReaderDisplayMessage',
      (data: any) => {
        return parseFloat(data.value);
      }
    );
  }

  public static async createPaymentIntent(
    amount: number,
    currency: string
  ): Promise<PaymentIntent> {
    this.ensureInitialized();

    const data = await MadStripeSdk.createPaymentIntent(amount, currency);
    return data?.intent ?? null;
  }

  public static async collectPaymentMethod(): Promise<PaymentIntent> {
    this.ensureInitialized();

    const data = await MadStripeSdk.collectPaymentMethod();

    return data?.intent ?? null;
  }

  public static async create(
    options: StripeTerminalConfig
  ): Promise<StripeTerminalSDK> {
    const terminal = new StripeTerminalSDK(options);

    await terminal.init();

    return terminal;
  }

  public static isInitialized(): Promise<boolean> {
    return MadStripeSdk.isInitialized();
  }

  public static discoverReaders(
    discoveryMethod: DiscoveryMethod,
    simulated: boolean
  ): Observable<Reader[]> {
    this.ensureInitialized();

    return new Observable((subscriber) => {
      this.listener.addListener(
        'readerDiscoveryCompletion',
        (event: { readers?: Reader[] }) => {
          try {
            const readers =
              event!.readers!.map((reader: Reader) => {
                return reader;
              }) || [];
            subscriber.next(readers);
          } catch (error) {
            console.log(error);
          }
        }
      );
      // start discovery
      this.isDiscovering = true;
      MadStripeSdk.discoverReaders(discoveryMethod, simulated)
        .then(() => {
          this.isDiscovering = false;
          subscriber.complete();
        })
        .catch((err: any) => {
          this.isDiscovering = false;
          subscriber.error(err);
        });

      return {
        unsubscribe: () => {
          this.cancelDiscoverReaders();
        },
      };
    });
  }

  private static ensureInitialized() {
    if (!this.isInitialized) {
      throw new Error(
        'StripeTerminalSDK must be initialized before you can use any methods.'
      );
    }
  }

  public static async cancelDiscoverReaders(): Promise<void> {
    try {
      if (!this.isDiscovering) {
        return;
      }
      this.listener.removeListener('readerDiscoveryCompletion', () => {});
      await MadStripeSdk.cancelDiscoverReaders();
      this.isDiscovering = false;
    } catch (err) {
      console.log(err);
    }
  }

  public static async connectBluetoothReader(
    serialNumber: string,
    locationId: string
  ): Promise<Reader> {
    this.ensureInitialized();

    const data = await MadStripeSdk.connectBluetoothReader(
      serialNumber,
      locationId
    );

    return data?.reader ?? null;
  }

  public static async connectInternetReader(
    serialNumber: String,
    failIfInUse?: boolean
  ): Promise<Reader> {
    this.ensureInitialized();

    const data = await MadStripeSdk.connectInternetReader(
      serialNumber,
      failIfInUse
    );

    return data?.reader ?? null;
  }

  public static async getConnectedReader(): Promise<Reader> {
    this.ensureInitialized();

    const data = await MadStripeSdk.getConnectedReader();

    return data ?? null;
  }

  public static async disconnectReader(): Promise<void> {
    this.ensureInitialized();

    return await MadStripeSdk.disconnectReader();
  }

  public static connectionStatus(): Observable<ConnectionStatus> {
    this.ensureInitialized();

    return new Observable((subscriber) => {
      let hasSentEvent = false;

      // get current value
      MadStripeSdk.getConnectionStatus()
        .then((status: any) => {
          // only send the initial value if the event listener hasn't already
          if (!hasSentEvent) {
            subscriber.next(this.translateConnectionStatus(status));
          }
        })
        .catch((err: any) => {
          subscriber.error(err);
        });

      // then listen for changes
      this.listener.addListener('didChangeConnectionStatus', (status: any) => {
        hasSentEvent = true;
        subscriber.next(this.translateConnectionStatus(status));
      });

      return {
        unsubscribe: () => {
          this.listener.removeListener('didChangeConnectionStatus', () => {});
        },
      };
    });
  }

  public static async clearCachedCredentials(): Promise<void> {
    this.ensureInitialized();

    return await MadStripeSdk.clearCachedCredentials();
  }

  public static async cancelCollectPaymentMethod(): Promise<void> {
    this.ensureInitialized();

    return await MadStripeSdk.cancelCollectPaymentMethod();
  }

  public static async processPayment(): Promise<PaymentIntent> {
    this.ensureInitialized();

    const data = await MadStripeSdk.confirmPaymentIntent();

    return data?.intent ?? null;
  }

  public static async installAvailableUpdate(): Promise<void> {
    this.ensureInitialized();

    return await MadStripeSdk.installAvailableUpdate();
  }

  private static translateConnectionStatus(data: {
    status: ConnectionStatus;
    isAndroid?: boolean;
  }): ConnectionStatus {
    let status: ConnectionStatus = data.status;

    if (data.isAndroid) {
      // the connection status on android is different than on iOS so we have to translate it
      status = AndroidConnectionStatusMap[data.status];
    }

    return status;
  }
}
