import {
  EventSubscription,
  NativeEventEmitter,
  NativeModules,
} from 'react-native';
import {
  callOnCanceledCallback,
  callOnCompleteCallback,
  callOnErrorCallback,
  processThemeValues,
} from './util';
import { Fields, RawInquiryField } from './fields';
import { InquiryEvent } from './InquiryEvent';
import PersonaInquiryView, {
  onPersonaInquiryViewEvent,
} from './PersonaInquiryView';
import { Versions } from './versions';
import type {
  CollectedData,
  ExternalCollectedData,
  ExtraData,
  OnCanceledCallback,
  OnCompleteCallback,
  OnErrorCallback,
  OnEventCallback,
} from './callbacks';
import {
  StepData,
  Document,
  UiStepData,
  SelfieCaptureMethod,
  SelfieCapture,
  SelfieStepData,
  GovernmentIdCaptureFrames,
  GovernmentIdCaptureSide,
  GovernmentIdCaptureMethod,
  GovernmentIdCapture,
  GovernmentIdStepData,
  DocumentStepData,
} from './StepData';

export { Fields };

export { PersonaInquiryView };

export { Versions };

export {
  CollectedData,
  ExtraData,
  OnCanceledCallback,
  OnCompleteCallback,
  OnErrorCallback,
  OnEventCallback,
};

export {
  StepData,
  Document,
  UiStepData,
  SelfieCaptureMethod,
  SelfieCapture,
  SelfieStepData,
  GovernmentIdCaptureFrames,
  GovernmentIdCaptureSide,
  GovernmentIdCaptureMethod,
  GovernmentIdCapture,
  GovernmentIdStepData,
  DocumentStepData,
};

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',
}

/**
 * String enum for style variants.
 */
export enum StyleVariant {
  LIGHT = 'light',
  DARK = 'dark',
}

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

  // Fields
  fields?: Fields;

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

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

const eventEmitter = new NativeEventEmitter(PersonaInquiry2);

let onEventCallback: OnEventCallback | null = null;

eventEmitter.addListener(
  'onEvent',
  (rawEvent: {
    event: {
      type: string;
    };
  }) => {
    let event = InquiryEvent.fromJson(rawEvent.event);

    if (event != null) {
      onEventCallback?.(event);
      onPersonaInquiryViewEvent(event);
    }
  }
);

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

  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.shareToken = options.shareToken;
    this.fields = options.fields;
    this.returnCollectedData = options.returnCollectedData;
    this.locale = options.locale;
    this.styleVariant = options.styleVariant;
    this.disablePresentationAnimation = options.disablePresentationAnimation;

    // 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));
  }

  static onEvent(callback: OnEventCallback) {
    onEventCallback = callback;
  }

  toOptionsJson() {
    return {
      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,
      shareToken: this.shareToken,
      fields: this.fields,
      returnCollectedData: this.returnCollectedData,
      themeSource: this.themeSource,
      iosTheme: processThemeValues(this.iosThemeObject || {}),
      locale: this.locale,
      styleVariant: this.styleVariant,
      disablePresentationAnimation: this.disablePresentationAnimation,
    };
  }

  /**
   * Launch the Persona Inquiry.
   */
  start() {
    this.onCompleteListener = eventEmitter.addListener(
      'onComplete',
      (event: {
        inquiryId: string;
        status: string;
        fields: Record<string, RawInquiryField>;
        collectedData: ExternalCollectedData | null;
      }) => {
        callOnCompleteCallback(event, this.onComplete);
        this.clearListeners();
      }
    );

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

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

    PersonaInquiry2.startInquiry(this.toOptionsJson());
  }
}

class InquiryBuilder {
  private _inquiryId: InquiryId;
  private _sessionToken?: string;
  private _shareToken?: 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;
  private _styleVariant?: StyleVariant;
  private _disablePresentationAnimation?: boolean;

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

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

    return this;
  }

  /**
   * Sets a share token (`cnst_*`) used to enable cross-organization data
   * sharing consent in the Persona inquiry flow. When provided, the SDK
   * forwards the token on every transition request so that the backend can
   * stage a consent step during the inquiry.
   *
   * The token is treated as an opaque string and is optional. Existing flows
   * without a share token are unaffected.
   *
   * @param shareToken share token from your server
   */
  shareToken(shareToken: string): InquiryBuilder {
    this._shareToken = shareToken;

    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;
  }

  styleVariant(styleVariant: StyleVariant): InquiryBuilder {
    this._styleVariant = styleVariant;

    return this;
  }

  disablePresentationAnimation(
    disablePresentationAnimation: boolean
  ): InquiryBuilder {
    this._disablePresentationAnimation = disablePresentationAnimation;

    return this;
  }

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

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 _shareToken?: string;
  private _returnCollectedData?: boolean;
  private _locale?: string;
  private _styleVariant?: StyleVariant;
  private _disablePresentationAnimation?: boolean;

  // 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;
  }

  /**
   * Sets a share token (`cnst_*`) used to enable cross-organization data
   * sharing consent in the Persona inquiry flow. When provided, the SDK
   * forwards the token on every transition request so that the backend can
   * stage a consent step during the inquiry.
   *
   * The token is treated as an opaque string and is optional. Existing flows
   * without a share token are unaffected.
   *
   * @param shareToken share token from your server
   */
  shareToken(shareToken: string): TemplateBuilder {
    this._shareToken = shareToken;

    return this;
  }

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

    return this;
  }

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

    return this;
  }

  styleVariant(styleVariant: StyleVariant): TemplateBuilder {
    this._styleVariant = styleVariant;

    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;
  }

  disablePresentationAnimation(
    disablePresentationAnimation: boolean
  ): TemplateBuilder {
    this._disablePresentationAnimation = disablePresentationAnimation;

    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,
      shareToken: this._shareToken,
      fields: this._fields,
      onComplete: this._onComplete,
      onCanceled: this._onCanceled,
      onError: this._onError,
      iosThemeObject: this._iosThemeObject,
      themeSource: this._themeSource,
      returnCollectedData: this._returnCollectedData,
      locale: this._locale,
      styleVariant: this._styleVariant,
      disablePresentationAnimation: this._disablePresentationAnimation,
    });
  }
}

/**
 * @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;
