import NativeInterface from './internal/nativeInterface';
import {
  IDapiConfigurations,
  IAccount,
  ICard,
  IBeneficiary,
  IWireBeneficiary,
  IDapiConnection,
  IPair,
  IAccountResponse,
  IIdentityResponse,
  ITransactionResponse,
  IAccountsMetadataResponse,
  DapiEnvironment,
  IBankBeneficiaryResponse,
  IBankWireBeneficiaryResponse,
  IDapiResult,
  DapiEndpoint,
  ITransferResponse,
  ICardResponse,
  ICardBalance,
  ILineAddress,
  IAccountBalance,
  DapiTransactionsType,
  IDapiThemeConfigurations,
  DapiTheme,
  DapiLanguage,
} from './internal/types';

export class DapiError extends Error {
  type?: string
  operationID?: string
  account?: string
  beneficiaryCoolDownPeriod?: {
    value: number;
    unit: string;
  };
  constructor(message: string, type?: string, operationID?: string, account?: string, beneficiaryCoolDownPeriod?: {
    value: number;
    unit: string;
  }) {
    super(message);
    this.name = 'DapiError';
    this.type = type;
    this.operationID = operationID;
    this.account = account;
    this.beneficiaryCoolDownPeriod = beneficiaryCoolDownPeriod;
  }
}

export class DapiConfigurations implements IDapiConfigurations {
  environment?: DapiEnvironment
  countries?: string[];
  endpoints?: Map<DapiEndpoint, string> | undefined;
  endPointExtraQueryItems?:  object | undefined;
  endPointExtraHeaderFields?: object| undefined;
  endPointExtraBody?: object | undefined;
  showLogos?: boolean | undefined;
  showCloseButton?: boolean | undefined;
  showAddButton?: boolean | undefined;
  showTransferSuccessfulResult?: boolean | undefined;
  showTransferErrorResult?: boolean | undefined;
  showExperimentalBanks?: boolean;
  postSuccessfulConnectionLoadingText?: string
  theme: IDapiThemeConfigurations | undefined
  language?: DapiLanguage | undefined;

  constructor(countries: string[], environment: DapiEnvironment = DapiEnvironment.production) {
    this.environment = environment;
    this.countries = countries;
  }
}

class TransferResponse implements ITransferResponse {
  accountID?: string;
  amount: number;
  operationID: string;
  remark?: string;
  reference?: string;

  constructor(amount: number, operationID: string, accountID?: string, remark?: string, reference?: string) {
    this.accountID = accountID;
    this.amount = amount;
    this.operationID = operationID;
    this.remark = remark;
    this.reference = reference;

  }
}

export class DapiThemeConfigurations implements IDapiThemeConfigurations {
  enforceTheme?: DapiTheme
  primaryColor?: object

  constructor(enforceTheme: DapiTheme, primaryColor: object) {
    this.enforceTheme = enforceTheme;
    this.primaryColor = primaryColor;
  }
}

export class DapiConnection implements IDapiConnection {
  private _clientUserID: string;
  private _userID: string;
  private _bankID: string;
  private _swiftCode: string;
  private _country: string;
  private _bankShortName: string;
  private _bankFullName: string;
  private _fullLogo: string;
  private _halfLogo: string;
  private _miniLogo: string;

  public get clientUserID(): string {
    return this._clientUserID;
  }
  public get userID(): string {
    return this._userID;
  }
  public get bankID(): string {
    return this._bankID;
  }
  public get swiftCode(): string {
    return this._swiftCode;
  }
  public get country(): string {
    return this._country;
  }
  public get bankShortName(): string {
    return this._bankShortName;
  }
  public get bankFullName(): string {
    return this._bankFullName;
  }
  public get fullLogo(): string {
    return this._fullLogo;
  }
  public get halfLogo(): string {
    return this._halfLogo;
  }
  public get miniLogo(): string {
    return this._miniLogo;
  }

  static async create(jsonConnectionDetails: string): Promise<IDapiConnection> {

    var promise = new Promise<IDapiConnection>(async (resolve, reject) => {

      const isStarted = await Dapi.instance.isStarted();

      if (!isStarted) {
        let error = new DapiError("Dapi SDK is not started yet. It's not permitted to call create() (or any other method) on Dapi SDK unless started.");
        reject(error);
        return;
      }

      NativeInterface.createConnection(jsonConnectionDetails).then(newJSONConnection => {
        Dapi.instance.getConnections().then(connections => {

          let isResolved = false;

          for (let index = 0; index < connections.length; index++) {
            const element = connections[index];
            if (element.userID === newJSONConnection.userID) {
              isResolved = true;
              resolve(element);
              break;
            }
          }

          if (!isResolved) {
            let error = new DapiError("matchingConnection is undefined");
            reject(error);
          }
        }).catch(getConnectionsError => {
          reject(getConnectionsError);
        });
      }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
      });
    });

    return promise;
  }

  constructor(
    clientUserID: string,
    userID: string,
    bankID: string,
    swiftCode: string,
    country: string,
    bankShortName: string,
    bankFullName: string,
    fullLogo: string,
    halfLogo: string,
    miniLogo: string,

  ) {
    this._clientUserID = clientUserID;
    this._userID = userID;
    this._bankID = bankID;
    this._swiftCode = swiftCode;
    this._country = country;
    this._bankShortName = bankShortName;
    this._bankFullName = bankFullName;
    this._fullLogo = fullLogo;
    this._halfLogo = halfLogo;
    this._miniLogo = miniLogo;

  }

async presentAccountSelection(): Promise < string | undefined > {
    return new Promise < string | undefined > (async (resolve, reject) => {
        NativeInterface.presentAccountSelection(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
            let { message, type , operationID} = errorInfo(error);
            reject(new DapiError(message, type, operationID));
        })
    });
}

getParameters(): Promise < string > {
    return new Promise < string > (async (resolve, reject) => {
        NativeInterface.getConnectionParameters(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
            let { message, type , operationID} = errorInfo(error);
            reject(new DapiError(message, type, operationID));
        })
    });
}

getIdentity(): Promise < IIdentityResponse > {
    return new Promise < IIdentityResponse > (async (resolve, reject) => {
        NativeInterface.getIdentity(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}


getAccounts(): Promise < IAccountResponse > {
    return new Promise < IAccountResponse > (async (resolve, reject) => {
        NativeInterface.getAccounts(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

getCards(): Promise < ICardResponse > {
    return new Promise < ICardResponse > (async (resolve, reject) => {
        NativeInterface.getCards(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

getTransactionsForAccount(
    account: IAccount,
    startDate: Date,
    endDate: Date,
    type: DapiTransactionsType
): Promise < ITransactionResponse > {
    return new Promise < ITransactionResponse > (async (resolve, reject) => {
        NativeInterface.getTransactionsForAccount(
            this.userID,
            account.id,
            startDate.getTime(),
            endDate.getTime(),
            type
        ).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

getTransactionsForCard(
    card: ICard,
    startDate: Date,
    endDate: Date,
    type: DapiTransactionsType,
): Promise < ITransactionResponse > {
    return new Promise < ITransactionResponse > (async (resolve, reject) => {
        NativeInterface.getTransactionsForCard(
            this.userID,
            card.id,
            startDate.getTime(),
            endDate.getTime(),
            type
        ).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

getAccountsMetadata(): Promise < IAccountsMetadataResponse > {
    return new Promise < IAccountsMetadataResponse > (async (resolve, reject) => {
        NativeInterface.getAccountsMetadata(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

delete(): Promise < void > {
    return new Promise < void > (async (resolve, reject) => {
        NativeInterface.delete(this.userID).then(() => {
            resolve();
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

createBeneficiary(beneficiary: IBeneficiary): Promise < IDapiResult > {
  return new Promise < IDapiResult > (async (resolve, reject) => {
      NativeInterface.createBeneficiary(this.userID, beneficiary).then(response => {
          resolve(response);
      }).catch(error => {
        let { message, type, operationID, unit, value} = errorInfo(error);
        reject(new DapiError(message, type, operationID, undefined, {value, unit}));
      })
  });
}

getBeneficiaries(): Promise < IBankBeneficiaryResponse > {
  return new Promise < IBankBeneficiaryResponse > (async (resolve, reject) => {
      NativeInterface.getBeneficiaries(this.userID).then(response => {
          resolve(response);
      }).catch(error => {
        let { message, type , operationID} = errorInfo(error);
        reject(new DapiError(message, type, operationID));
      })
  });
}

async createTransfer(fromAccount: IAccount | null, toBeneficiary: IBeneficiary | null, amount: number, remark: string | null): Promise<ITransferResponse> {
  return new Promise < ITransferResponse > (async (resolve, reject) => {
    await NativeInterface.createTransfer(
      this.userID,
      fromAccount ? fromAccount.id : null,
      toBeneficiary,
      amount,
      remark,
    ).then(response => {
      let accountID = response.account;
      let amnt = response.amount;
      let operationID = response.operationID;
      let resultRemark = response.remark;
      let reference = response.reference;
      resolve(new TransferResponse(amnt, operationID, accountID, resultRemark, reference));
    }).catch(error => {
      let { message, type , operationID, account, unit, value} = errorInfo(error);
      reject(new DapiError(message, type, operationID, account, {value, unit}));
    });
  });
  
}

async createTransferToExistingBeneficiary(
  fromAccount: IAccount,
  toBeneficiaryID: string,
  amount: number,
  remark: string | null,
): Promise<ITransferResponse> {
    return new Promise < ITransferResponse > (async (resolve, reject) => {
      await NativeInterface.createTransferToExistingBeneficiary(
        this.userID,
        fromAccount.id,
        toBeneficiaryID,
        amount,
        remark,
      ).then(response => {
        let accountID = response.account;
        let amnt = response.amount;
        let operationID = response.operationID;
        let resultRemark = response.remark;
        let reference = response.reference;
        resolve(new TransferResponse(amnt, operationID, accountID, resultRemark, reference));
      }).catch(error => {
        let { message, type , operationID, account, unit, value} = errorInfo(error);
        reject(new DapiError(message, type, operationID, account, {value, unit}));
      });
    });
}

createWireBeneficiary(beneficiary: IWireBeneficiary): Promise < IDapiResult > {
    return new Promise < IDapiResult > (async (resolve, reject) => {
        NativeInterface.createWireBeneficiary(this.userID, beneficiary).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID, unit, value} = errorInfo(error);
          reject(new DapiError(message, type, operationID, undefined, {value, unit}));
        })
    });
}

getWireBeneficiaries(): Promise < IBankWireBeneficiaryResponse > {
    return new Promise < IBankWireBeneficiaryResponse > (async (resolve, reject) => {
        NativeInterface.getWireBeneficiaries(this.userID).then(response => {
            resolve(response);
        }).catch(error => {
          let { message, type , operationID} = errorInfo(error);
          reject(new DapiError(message, type, operationID));
        })
    });
}

async createWireTransfer(toBeneficiary: IWireBeneficiary, fromAccount: IAccount | null, amount: number, remark: string | null): Promise<ITransferResponse> {
    return new Promise < ITransferResponse > (async (resolve, reject) => {
      await NativeInterface.createWireTransfer(
        this.userID,
        toBeneficiary,
        fromAccount ? fromAccount.id : null,
        amount,
        remark,
      ).then(response => {
        let accountID = response.account;
        let amnt = response.amount;
        let operationID = response.operationID;
        let resultRemark = response.remark;
        let reference = response.reference;
        resolve(new TransferResponse(amnt, operationID, accountID, resultRemark, reference));
      }).catch(error => {
        let { message, type , operationID, account, unit, value} = errorInfo(error);
        reject(new DapiError(message, type, operationID, account, {value, unit}));
      });
    });
}

async createWireTransferToExistingBeneficiary(
  fromAccount: IAccount,
  toBeneficiaryID: string,
  amount: number,
  remark: string | null,
): Promise<ITransferResponse> {
    return new Promise < ITransferResponse > (async (resolve, reject) => {
      await NativeInterface.createWireTransferToExistingBeneficiary(
        this.userID,
        fromAccount.id,
        toBeneficiaryID,
        amount,
        remark,
      ).then(response => {
        let accountID = response.account;
        let amnt = response.amount;
        let operationID = response.operationID;
        let resultRemark = response.remark;
        let reference = response.reference;
        resolve(new TransferResponse(amnt, operationID, accountID, resultRemark, reference));
      }).catch(error => {
        let { message, type , operationID, account, unit, value} = errorInfo(error);
        reject(new DapiError(message, type, operationID, account, {value, unit}));
      });
    });
}

async createACHPullTransfer(description: string, fromAccount: IAccount | null, amount: number): Promise<ITransferResponse> {
  return new Promise < ITransferResponse > (async (resolve, reject) => {
    await NativeInterface.createACHPullTransfer(
      this.userID,
      description,
      fromAccount ? fromAccount.id : null,
      amount,
    ).then(response => {
      let accountID = response.account;
      let amnt = response.amount;
      let operationID = response.operationID;
      resolve(new TransferResponse(amnt, operationID, accountID, undefined, undefined));
    }).catch(error => {
      let { message, type , operationID, account, unit, value} = errorInfo(error);
      reject(new DapiError(message, type, operationID, account, {value, unit}));
    });
  });
}

async nymcardLoadFunds(token: string, fromAccount: IAccount | null, amount: number): Promise<ITransferResponse> {
  return new Promise < ITransferResponse > (async (resolve, reject) => {
    await NativeInterface.nymcardLoadFunds(
      this.userID,
      token,
      fromAccount ? fromAccount.id : null,
      amount,
    ).then(response => {
      let accountID = response.account;
      let amnt = response.amount;
      let operationID = response.operationID;
      let remark = response.remark;
      let reference = response.reference;
      resolve(new TransferResponse(amnt, operationID, accountID, remark, reference));
    }).catch(error => {
      let { message, type , operationID, account, unit, value} = errorInfo(error);
      reject(new DapiError(message, type, operationID, account, {value, unit}));
    });
  });
}

}

export class DapiPair implements IPair {
  code: string;
  name: string;

  constructor(code: string, name: string) {
    this.code = code;
    this.name = name;
  }
}

export class DapiLineAddress implements ILineAddress {
  line1: string;
  line2: string;
  line3: string;

  constructor(
    line1: string,
    line2: string,
    line3: string
  ) {
    this.line1 = line1;
    this.line2 = line2;
    this.line3 = line3;
  }
}


export class DapiBeneficiary implements IBeneficiary {
  linesAddress: ILineAddress;
  accountNumber: string;
  name: string;
  bankName: string;
  swiftCode: string;
  iban: string;
  phoneNumber: string;
  country: string;
  branchAddress: string;
  branchName: string;
  nickname?: string | null;

  constructor(
    linesAddress: ILineAddress,
    accountNumber: string,
    name: string,
    bankName: string,
    swiftCode: string,
    iban: string,
    phoneNumber: string,
    country: string,
    branchAddress: string,
    branchName: string,
    nickname?: string | null
  ) {
    this.linesAddress = linesAddress;
    this.accountNumber = accountNumber;
    this.name = name;
    this.bankName = bankName;
    this.swiftCode = swiftCode;
    this.iban = iban;
    this.phoneNumber = phoneNumber;
    this.country = country;
    this.branchAddress = branchAddress;
    this.branchName = branchName;
    this.nickname = nickname
  }

}

export class DapiWireBeneficiary implements IWireBeneficiary {
  linesAddress: ILineAddress;
  name: string
  firstName: string
  lastName: string
  nickname: string
  city: string
  state: string
  country: string
  zipCode: string
  receiverType: string
  receiverAccountType: string
  routingNumber: string
  accountNumber: string

  constructor(
    linesAddress: ILineAddress,
    name: string,
    firstName: string,
    lastName: string,
    nickname: string,
    city: string,
    state: string,
    country: string,
    zipCode: string,
    receiverType: string,
    receiverAccountType: string,
    routingNumber: string,
    accountNumber: string,
  ) {
    this.linesAddress = linesAddress
    this.name = name
    this.firstName = firstName
    this.lastName = lastName
    this.nickname = nickname
    this.city = city
    this.state = state
    this.country = country
    this.zipCode = zipCode
    this.receiverType = receiverType
    this.receiverAccountType = receiverAccountType
    this.routingNumber = routingNumber
    this.accountNumber = accountNumber
  }

}


export class DapiAccount implements IAccount {
  balance: IAccountBalance;
  iban: string | null;
  number: string | null;
  currency: IPair;
  type: string;
  id: string;
  name: string;

  constructor(
    balance: IAccountBalance,
    iban: string | null,
    number: string | null,
    currency: IPair,
    type: string,
    id: string,
    name: string,
  ) {
    this.balance = balance;
    this.iban = iban;
    this.number = number;
    this.currency = currency;
    this.type = type;
    this.id = id;
    this.name = name;
  }
}

export class DapiCardBalance implements ICardBalance {
  readonly amountDue: number;
  readonly availableBalance: number;
  readonly outstandingBalance: number;
  readonly dueDate: string;

  constructor(amountDue: number, availableBalance: number, outstandingBalance: number, dueDate: string) {
    this.amountDue = amountDue;
    this.availableBalance = availableBalance;
    this.outstandingBalance = outstandingBalance;
    this.dueDate = dueDate;
  }

}

export class DapiCard implements ICard {
  balance: DapiCardBalance;
  cardNumber: string;
  creditLimit: string;
  currency: IPair;
  expiryDate: string;
  id: string;
  name: string;
  status: string;
  type: string;

  constructor(balance: DapiCardBalance, cardNumber: string, creditLimit: string, currency: IPair, expiryDate: string, id: string, name: string, status: string, type: string) {
    this.balance = balance;
    this.cardNumber = cardNumber;
    this.creditLimit = creditLimit;
    this.currency = currency;
    this.expiryDate = expiryDate;
    this.id = id;
    this.name = name;
    this.status = status;
    this.type = type;
  }
}

export default class Dapi {
  private static _instance = new Dapi();
  public static get instance(): Dapi {
    return this._instance;
  }
  private constructor() { }

  start(
    appKey: string,
    clientUserID: string,
    configurations: IDapiConfigurations,
  ): Promise<void> {
    return new Promise < void > (async (resolve, reject) => {
      NativeInterface.start(appKey, clientUserID, configurations).then(response => {
          resolve(response);
      }).catch(error => {
        let { message, type , operationID} = errorInfo(error);
        reject(new DapiError(message, type, operationID));
      })
    });
  }

  isStarted(): Promise<boolean> {
    return NativeInterface.isStarted();
  }

  presentConnect(bankID?: string | null): void {
    NativeInterface.presentConnect(bankID);
  }

  setClientUserID(clientUserID: string): void {
    NativeInterface.setClientUserID(clientUserID);
  }

  clientUserID(): Promise<string> {
    return NativeInterface.clientUserID();
  }

  setConfigurations(configurations: IDapiConfigurations): void {
    NativeInterface.setConfigurations(configurations);
  }

  configurations(): Promise<IDapiConfigurations> {
    return NativeInterface.configurations();
  }

  dismissConnect(): void {
    NativeInterface.dismissConnect();
  }

  async getConnections(): Promise<IDapiConnection[]> {
    return new Promise < IDapiConnection[] > (async (resolve, reject) => {
      await NativeInterface.getConnections().then(response => {
        let connections: IDapiConnection[] = [];
        for (let i = 0; i < response.length; i++) {
          let currentConnection = response[i];
          let connection = new DapiConnection(
            currentConnection.clientUserID,
            currentConnection.userID,
            currentConnection.bankID,
            currentConnection.swiftCode,
            currentConnection.country,
            currentConnection.bankShortName,
            currentConnection.bankFullName,
            currentConnection.fullLogo,
            currentConnection.halfLogo,
            currentConnection.miniLogo
          );
          connections.push(connection);
        }
        resolve(connections);
      }).catch(error => {
        let { message, type , operationID} = errorInfo(error);
        reject(new DapiError(message, type, operationID));
      })
    });
  }
}

function errorInfo(error: any) {
  let json = JSON.parse(error.message);

  let message;
  let type;
  let operationID;
  let account;
  let unit;
  let value;

  if(json.hasOwnProperty("message")) {
    message = json.message;
  }

  if(json.hasOwnProperty("type")) {
    type = json.type;
  }

  if(json.hasOwnProperty("operationID")) {
    operationID = json.operationID;
  }

  if(json.hasOwnProperty("account")) {
    account = json.account;
  }

  if(json.hasOwnProperty("unit")) {
    unit = json.unit;
  }

  if(json.hasOwnProperty("value")) {
    value = json.value;
  }

  return { message: message, type: type, operationID: operationID, account: account, unit: unit, value: value };
}

export * from './internal/types';


