import {
  EventSubscription,
  NativeEventEmitter,
  NativeModules,
} from 'react-native';
import { processThemeValues } from './util';
import { Fields, InquiryField, RawInquiryField } from './fields';

export { Fields };

import { Versions } from './versions';

export { Versions };

const { PersonaInquiry2 } = NativeModules;

// Using Opaque types + Smart Constructor enforces validation at
// instantiation time for IDS
declare const Unique: unique symbol;
export type Opaque<T, Tag> = T & { [Unique]: Tag };

type TemplateId = Opaque<string, 'TemplateId'>;
type TemplateVersion = Opaque<string, 'TemplateVersion'>;
type InquiryId = Opaque<string, 'InquiryId'>;
type AccountId = Opaque<string, 'AccountId'>;

export class InvalidTemplateId extends Error {}

export class InvalidTemplateVersion extends Error {}

export class InvalidInquiryId extends Error {}

export class InvalidAccountId extends Error {}

/**
 * Run validations that the string is in proper Inquiry token format
 * and do a type conversion to InquiryId.
 *
 * @param candidate
 */
function makeInquiryId(candidate: string): InquiryId {
  if (candidate && candidate.startsWith('inq_')) {
    return candidate as InquiryId;
  }

  throw new InvalidInquiryId(
    `Valid template IDs start with "inq_". Received: ${candidate} `
  );
}

/**
 * Run validations that the string is in proper Template token format
 * and do a type conversion to TemplateId.
 *
 * @param candidate
 */
function makeTemplateId(candidate: string): TemplateId {
  if (candidate && candidate.startsWith('itmpl_')) {
    return candidate as TemplateId;
  }

  throw new InvalidTemplateId(
    `Valid template IDs start with "itmpl_". Received: ${candidate} `
  );
}

/**
 * Run validations that the string is in proper Template Version token format
 * and do a type conversion to TemplateVersion.
 *
 * @param candidate
 */
function makeTemplateVersion(candidate: string): TemplateVersion {
  if (candidate && candidate.startsWith('itmplv_')) {
    return candidate as TemplateVersion;
  }

  throw new InvalidTemplateVersion(
    `Valid template versions start with "itmplv_". Received: ${candidate} `
  );
}

/**
 * Run validations that the string is in proper Template token format
 * and do a type conversion to AccountId.
 *
 * @param candidate
 */
function makeAccountId(candidate: string): AccountId {
  if (candidate && candidate.startsWith('act_')) {
    return candidate as AccountId;
  }

  throw new InvalidAccountId(
    `Valid account IDs start with "act_". Received: ${candidate} `
  );
}

/**
 * String enum for environments. These strings will be parsed
 * on the native side bridge into Kotlin / Swift enums.
 */
export enum Environment {
  SANDBOX = 'sandbox',
  PRODUCTION = 'production',
}

/**
 * An enum value which determines whether this sdk should use the theme values sent from the server.
 */
export enum ThemeSource {
  SERVER = 'server',
  /**
   * @deprecated Client side theming is deprecated, please configure your theme inside
   * the Persona Dashboard and use SERVER as the theme source.
   */
  CLIENT = 'client',
}

export interface InquiryOptions {
  templateId?: TemplateId;
  templateVersion?: TemplateVersion;
  inquiryId?: InquiryId;
  referenceId?: string;
  accountId?: AccountId;
  environment?: Environment;
  environmentId?: string;
  themeSetId?: string;
  sessionToken?: string;
  returnCollectedData?: boolean;
  locale?: String;

  // Fields
  fields?: Fields;

  // Callbacks
  onComplete?: OnCompleteCallback;
  onCanceled?: OnCanceledCallback;
  onError?: OnErrorCallback;

  // Customization
  iosThemeObject?: Object | null;
  themeSource?: ThemeSource | null;
}

type OnCompleteCallback = (
  inquiryId: string,
  status: string,
  fields: Fields,
  extraData: ExtraData
) => void;

/**
 * Type for collected data that came directly from iOS/Android.
 * Needs to be translated to TS classes before returning.
 */
interface ExternalCollectedData {
  stepData: any[];
}

export interface ExtraData {
  collectedData: CollectedData | null;
}
export interface CollectedData {
  stepData: StepData[];
}

export interface StepData {
  stepName: string;
}

export class DocumentStepData implements StepData {
  stepName: string;
  documents: Document[];

  constructor() {
    this.stepName = '';
    this.documents = [];
  }
}

export interface Document {
  absoluteFilePath: string;
}

export class GovernmentIdStepData implements StepData {
  stepName: string;
  captures: GovernmentIdCapture[];

  constructor() {
    this.stepName = '';
    this.captures = [];
  }
}

export interface GovernmentIdCapture {
  idClass: string;
  captureMethod: GovernmentIdCaptureMethod;
  side: GovernmentIdCaptureSide;
  frames: GovernmentIdCaptureFrames[];
}

export enum GovernmentIdCaptureMethod {
  Manual = 'Manual',
  Auto = 'Auto',
  Upload = 'Upload',
}

export enum GovernmentIdCaptureSide {
  Front = 'Front',
  Back = 'Back',
}

export interface GovernmentIdCaptureFrames {
  absoluteFilePath: string;
}

export class SelfieStepData implements StepData {
  stepName: string;
  centerCapture: SelfieCapture | null;
  leftCapture: SelfieCapture | null;
  rightCapture: SelfieCapture | null;

  constructor() {
    this.stepName = '';
    this.centerCapture = null;
    this.leftCapture = null;
    this.rightCapture = null;
  }
}

export interface SelfieCapture {
  captureMethod: SelfieCaptureMethod;
  absoluteFilePath: string;
}

export enum SelfieCaptureMethod {
  Manual = 'Manual',
  Auto = 'Auto',
}

export class UiStepData implements StepData {
  stepName: string;
  componentParams: { [key: string]: any };

  constructor() {
    this.stepName = '';
    this.componentParams = {};
  }
}

type OnCanceledCallback = (inquiryId?: string, sessionToken?: string) => void;

type OnErrorCallback = (error: Error, errorCode?: string) => void;

const eventEmitter = new NativeEventEmitter(PersonaInquiry2);

export class Inquiry {
  templateId?: TemplateId;
  templateVersion?: TemplateVersion;
  inquiryId?: InquiryId;
  referenceId?: string;
  accountId?: AccountId;
  environment?: Environment;
  environmentId?: string;
  themeSetId?: string;
  sessionToken?: string;
  returnCollectedData?: boolean;
  locale?: String;

  iosThemeObject?: Object | null;
  themeSource?: ThemeSource | null;
  fields?: Fields | null;

  private readonly onComplete?: OnCompleteCallback;
  private readonly onCanceled?: OnCanceledCallback;
  private readonly onError?: OnErrorCallback;

  private onCompleteListener?: EventSubscription;
  private onCanceledListener?: EventSubscription;
  private onErrorListener?: EventSubscription;

  constructor(options: InquiryOptions) {
    this.templateId = options.templateId;
    this.templateVersion = options.templateVersion;
    this.inquiryId = options.inquiryId;
    this.referenceId = options.referenceId;
    this.accountId = options.accountId;
    this.environment = options.environment;
    this.environmentId = options.environmentId;
    this.themeSetId = options.themeSetId;
    this.sessionToken = options.sessionToken;
    this.fields = options.fields;
    this.returnCollectedData = options.returnCollectedData;
    this.locale = options.locale;

    // Callbacks
    this.onComplete = options.onComplete;
    this.onCanceled = options.onCanceled;
    this.onError = options.onError;

    // Theme object
    this.iosThemeObject = options.iosThemeObject;
    this.themeSource = options.themeSource;
  }

  private clearListeners() {
    if (this.onCompleteListener) this.onCompleteListener.remove();
    if (this.onCanceledListener) this.onCanceledListener.remove();
    if (this.onErrorListener) this.onErrorListener.remove();
  }

  /**
   * Create an Inquiry flow builder based on a template ID.
   *
   * You can find your template ID on the Dashboard under Inquiries > Templates.
   * {@link https://app.withpersona.com/dashboard/inquiry-templates}
   *
   * @param templateId template ID from your Persona Dashboard
   * @return builder for the Inquiry flow
   */
  static fromTemplate(templateId: string) {
    return new TemplateBuilder(makeTemplateId(templateId), null);
  }

  /**
   * Create an Inquiry flow builder based on a template ID version.
   *
   * You can find your template ID version on the Dashboard under the
   * settings view of a specific template.
   * {@link https://app.withpersona.com/dashboard/inquiry-templates}
   *
   * @param templateVersion template version from your Persona Dashboard
   * @return builder for the Inquiry flow
   */
  static fromTemplateVersion(templateVersion: string) {
    return new TemplateBuilder(null, makeTemplateVersion(templateVersion));
  }

  /**
   * Create an Inquiry flow builder based on an inquiry ID.
   *
   * You will need to generate the inquiry ID on the server. To try it out, you can create an
   * inquiry from the Persona Dashboard under "Inquiries". Click on the "Create Inquiry" button
   * and copy the inquiry ID from the URL.
   * {@link https://app.withpersona.com/dashboard/inquiries}
   *
   * @param inquiryId inquiry ID from your server
   * @return builder for the Inquiry flow
   */
  static fromInquiry(inquiryId: string) {
    return new InquiryBuilder(makeInquiryId(inquiryId));
  }

  /**
   * Launch the Persona Inquiry.
   */
  start() {
    this.onCompleteListener = eventEmitter.addListener(
      'onComplete',
      (event: {
        inquiryId: string;
        status: string;
        fields: Record<string, RawInquiryField>;
        collectedData: ExternalCollectedData | null;
      }) => {
        if (this.onComplete) {
          let fields: Fields = {};
          for (let key of Object.keys(event.fields || {})) {
            let field = event.fields[key];
            if (field == undefined) {
              fields[key] = new InquiryField.Unknown('null');
              continue;
            }
            switch (field.type) {
              case 'integer':
                fields[key] = new InquiryField.Integer(
                  Number.parseInt(field.value)
                );
                break;
              case 'boolean':
                fields[key] = new InquiryField.Boolean(field.value);
                break;
              case 'string':
                fields[key] = new InquiryField.String(field.value);
                break;
              default:
                fields[key] = new InquiryField.Unknown(field.type);
                break;
            }
          }

          let collectedData: CollectedData | null = null;

          let stepData = event.collectedData?.stepData;
          if (stepData != null) {
            // Translate the step data from JSON to actual class objects
            let translatedStepData = [];

            for (let stepDatum of stepData) {
              switch (stepDatum.type) {
                case 'DocumentStepData':
                  translatedStepData.push(
                    Object.assign(new DocumentStepData(), stepDatum)
                  );
                  break;
                case 'GovernmentIdStepData':
                  translatedStepData.push(
                    Object.assign(new GovernmentIdStepData(), stepDatum)
                  );
                  break;
                case 'SelfieStepData':
                  translatedStepData.push(
                    Object.assign(new SelfieStepData(), stepDatum)
                  );
                  break;
                case 'UiStepData':
                  translatedStepData.push(
                    Object.assign(new UiStepData(), stepDatum)
                  );
                  break;
              }
            }

            collectedData = {
              stepData: translatedStepData,
            };
          }

          let extraData: ExtraData = {
            collectedData: collectedData,
          };

          this.onComplete(event.inquiryId, event.status, fields, extraData);
        }
        this.clearListeners();
      }
    );

    this.onCanceledListener = eventEmitter.addListener(
      'onCanceled',
      (event: { inquiryId?: string; sessionToken?: string }) => {
        if (this.onCanceled)
          this.onCanceled(event.inquiryId, event.sessionToken);
        this.clearListeners();
      }
    );

    this.onErrorListener = eventEmitter.addListener(
      'onError',
      (event: { debugMessage: string; errorCode?: string }) => {
        if (this.onError)
          this.onError(new Error(event.debugMessage), event.errorCode);
        this.clearListeners();
      }
    );

    PersonaInquiry2.startInquiry({
      templateId: this.templateId,
      templateVersion: this.templateVersion,
      inquiryId: this.inquiryId,
      referenceId: this.referenceId,
      accountId: this.accountId,
      environment: this.environment,
      environmentId: this.environmentId,
      themeSetId: this.themeSetId,
      sessionToken: this.sessionToken,
      fields: this.fields,
      returnCollectedData: this.returnCollectedData,
      themeSource: this.themeSource,
      iosTheme: processThemeValues(this.iosThemeObject || {}),
      locale: this.locale,
    });
  }
}

class InquiryBuilder {
  private _inquiryId: InquiryId;
  private _sessionToken?: string;

  // Callbacks
  private _onComplete?: OnCompleteCallback;
  private _onCanceled?: OnCanceledCallback;
  private _onError?: OnErrorCallback;
  private _iosThemeObject?: Object;
  private _themeSource: ThemeSource = ThemeSource.SERVER;
  private _fields?: Fields;
  private _locale?: string;

  constructor(inquiryId: InquiryId) {
    this._inquiryId = inquiryId;
  }

  sessionToken(sessionToken: string): InquiryBuilder {
    this._sessionToken = sessionToken;

    return this;
  }

  onComplete(callback: OnCompleteCallback): InquiryBuilder {
    this._onComplete = callback;

    return this;
  }

  onCanceled(callback: OnCanceledCallback): InquiryBuilder {
    this._onCanceled = callback;

    return this;
  }

  onError(callback: OnErrorCallback): InquiryBuilder {
    this._onError = callback;

    return this;
  }

  /**
   * @deprecated Use iosThemeToUse
   */
  iosTheme(themeObject: Object): InquiryBuilder {
    this._iosThemeObject = themeObject;
    this._themeSource = ThemeSource.CLIENT;

    return this;
  }

  iosThemeToUse(themeObject: Object, themeSource: ThemeSource): InquiryBuilder {
    this._iosThemeObject = themeObject;
    this._themeSource = themeSource;

    return this;
  }

  locale(locale: string): InquiryBuilder {
    this._locale = locale;

    return this;
  }

  build(): Inquiry {
    return new Inquiry({
      inquiryId: this._inquiryId,
      sessionToken: this._sessionToken,
      onComplete: this._onComplete,
      onCanceled: this._onCanceled,
      onError: this._onError,
      iosThemeObject: this._iosThemeObject,
      themeSource: this._themeSource,
      fields: this._fields,
      locale: this._locale,
    });
  }
}

class TemplateBuilder {
  private readonly _templateId?: TemplateId;
  private readonly _templateVersion?: TemplateVersion;
  private _accountId?: AccountId;
  private _referenceId?: string;
  private _environment?: Environment;
  private _environmentId?: string;
  private _themeSetId?: string;
  private _fields?: Fields;
  private _sessionToken?: string;
  private _returnCollectedData?: boolean;
  private _locale?: string;

  // Callbacks
  private _onComplete?: OnCompleteCallback;
  private _onCanceled?: OnCanceledCallback;
  private _onError?: OnErrorCallback;

  // Customization
  private _iosThemeObject?: Object;
  private _themeSource: ThemeSource = ThemeSource.SERVER;

  constructor(
    templateId?: TemplateId | null,
    templateVersion?: TemplateVersion | null
  ) {
    if (templateId != null) {
      this._templateId = templateId;
    } else if (templateVersion != null) {
      this._templateVersion = templateVersion;
    } else {
      throw new InvalidTemplateId(
        `Either templateId or templateVersion needs to be set.`
      );
    }
    return this;
  }

  returnCollectedData(returnCollectedData: boolean) {
    this._returnCollectedData = returnCollectedData;

    return this;
  }

  referenceId(referenceId: string): TemplateBuilder {
    if (referenceId == null) {
      return this;
    }
    this._accountId = undefined;
    this._referenceId = referenceId;

    return this;
  }

  accountId(accountId: string): TemplateBuilder {
    if (accountId == null) {
      return this;
    }
    this._referenceId = undefined;
    this._accountId = makeAccountId(accountId);

    return this;
  }

  environment(environment: Environment): TemplateBuilder {
    this._environment = environment;

    return this;
  }

  environmentId(environmentId: string): TemplateBuilder {
    this._environmentId = environmentId;

    return this;
  }

  themeSetId(themeSetId: string): TemplateBuilder {
    this._themeSetId = themeSetId;

    return this;
  }

  sessionToken(sessionToken: string): TemplateBuilder {
    this._sessionToken = sessionToken;

    return this;
  }

  fields(fields: Fields): TemplateBuilder {
    this._fields = fields;

    return this;
  }

  locale(locale: string): TemplateBuilder {
    this._locale = locale;

    return this;
  }

  onComplete(callback: OnCompleteCallback): TemplateBuilder {
    this._onComplete = callback;

    return this;
  }

  onCanceled(callback: OnCanceledCallback): TemplateBuilder {
    this._onCanceled = callback;

    return this;
  }

  onError(callback: OnErrorCallback): TemplateBuilder {
    this._onError = callback;

    return this;
  }

  /**
   * @deprecated Use iosThemeToUse
   */
  iosTheme(themeObject: Object): TemplateBuilder {
    this._iosThemeObject = themeObject;
    this._themeSource = ThemeSource.CLIENT;

    return this;
  }

  iosThemeToUse(
    themeObject: Object,
    themeSource: ThemeSource
  ): TemplateBuilder {
    this._iosThemeObject = themeObject;
    this._themeSource = themeSource;

    return this;
  }

  build(): Inquiry {
    return new Inquiry({
      templateId: this._templateId,
      templateVersion: this._templateVersion,
      accountId: this._accountId,
      referenceId: this._referenceId,
      environment: this._environment,
      environmentId: this._environmentId,
      themeSetId: this._themeSetId,
      sessionToken: this._sessionToken,
      fields: this._fields,
      onComplete: this._onComplete,
      onCanceled: this._onCanceled,
      onError: this._onError,
      iosThemeObject: this._iosThemeObject,
      themeSource: this._themeSource,
      returnCollectedData: this._returnCollectedData,
      locale: this._locale,
    });
  }
}

/**
 * @deprecated Use the `Inquiry` static methods instead
 */
namespace InquiryBuilders {
  /**
   * @deprecated Use {@link Inquiry#fromInquiry} instead
   */
  export function fromInquiry(inquiryId: string) {
    return Inquiry.fromInquiry(inquiryId);
  }

  /**
   * @deprecated Use {@link Inquiry#fromTemplate} instead
   */
  export function fromTemplate(templateId: string) {
    return Inquiry.fromTemplate(templateId);
  }

  /**
   * @deprecated Use {@link Inquiry#fromTemplateVersion} instead
   */
  export function fromTemplateVersion(templateVersion: string) {
    return Inquiry.fromTemplateVersion(templateVersion);
  }
}

export default InquiryBuilders;
