/* eslint-disable @typescript-eslint/lines-between-class-members */
import security from '@blocklet/sdk/lib/security';
import OcapClient from '@ocap/client';
import { JsonRpcProvider, ethers } from 'ethers';
import cloneDeep from 'lodash/cloneDeep';
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import Stripe from 'stripe';
import type { LiteralUnion } from 'type-fest';

import { AppStoreClient } from '../../integrations/app-store/client';
import { GooglePlayClient } from '../../integrations/google-play/client';
import { STRIPE_API_VERSION, createIdGenerator } from '../../libs/util';
import { sequelize } from '../sequelize';
import type { PaymentMethodSettings } from './types';
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../../libs/constants';

const nextId = createIdGenerator('pm', 24);

const stripeClients = new Map<string, Stripe>();
const evmClients = new Map<string, JsonRpcProvider>();
const ocapClients = new Map<string, OcapClient>();
const googlePlayClients = new Map<string, GooglePlayClient>();
const appStoreClients = new Map<string, AppStoreClient>();

// eslint-disable-next-line prettier/prettier
export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCreationAttributes<PaymentMethod>> {
  // Unique identifier for the object.
  declare id: CreationOptional<string>;

  declare active: boolean;
  declare livemode: boolean;
  declare locked: boolean;

  declare type: LiteralUnion<
    'stripe' | 'arcblock' | 'ethereum' | 'bitcoin' | 'base' | 'app_store' | 'google_play',
    string
  >;

  declare name: string;

  declare description: string;

  declare logo: string;

  declare default_currency_id?: string;

  // How do we confirm transactions
  declare confirmation: {
    type: LiteralUnion<'immediate' | 'callback' | 'block', string>;
    block?: number;
  };

  // Extra settings to make the payment method work
  declare settings: PaymentMethodSettings;

  // What features are supported
  declare features: {
    recurring: boolean;
    refund: boolean;
    dispute: boolean;
  };

  declare metadata: Record<string, any>;

  declare created_at: CreationOptional<Date>;
  declare updated_at: CreationOptional<Date>;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(30),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextId,
    },
    active: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    locked: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    type: {
      type: DataTypes.ENUM('stripe', 'arcblock', 'ethereum', 'bitcoin', 'base'),
    },
    name: {
      type: DataTypes.STRING(64),
    },
    description: {
      type: DataTypes.STRING(512),
    },
    logo: {
      type: DataTypes.STRING(512),
    },
    default_currency_id: {
      type: DataTypes.STRING(15),
      allowNull: true,
    },
    confirmation: {
      type: DataTypes.JSON,
      allowNull: false,
    },
    settings: {
      type: DataTypes.JSON,
      allowNull: false,
    },
    features: {
      type: DataTypes.JSON,
      allowNull: false,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  // eslint-disable-next-line @typescript-eslint/no-shadow
  public static initialize(sequelize: any) {
    this.init(PaymentMethod.GENESIS_ATTRIBUTES, {
      sequelize,
      modelName: 'PaymentMethod',
      tableName: 'payment_methods',
      createdAt: 'created_at',
      updatedAt: 'updated_at',
    });
  }

  public static associate(models: any) {
    this.hasMany(models.PaymentCurrency, {
      foreignKey: 'payment_method_id',
      as: 'payment_currencies',
    });
  }

  public static expand(livemode?: boolean, conditions: Record<string, any> = {}) {
    return PaymentMethod.findAll({
      where: { livemode: !!livemode, active: true, ...conditions },
      order: [['created_at', 'DESC']],
      include: [{ model: sequelize.models.PaymentCurrency, as: 'payment_currencies' }],
    }).then((methods) => methods.map((x) => x.toJSON()));
  }

  public static encryptSettings(settings: PaymentMethodSettings) {
    const tmp = cloneDeep(settings);
    if (tmp.stripe) {
      tmp.stripe.secret_key = security.encrypt(tmp.stripe.secret_key);
      tmp.stripe.webhook_signing_secret = security.encrypt(tmp.stripe.webhook_signing_secret);
    }
    if (tmp.google_play) {
      tmp.google_play.service_account_json = security.encrypt(tmp.google_play.service_account_json);
    }
    if (tmp.app_store?.private_key_pem) {
      tmp.app_store.private_key_pem = security.encrypt(tmp.app_store.private_key_pem);
    }
    if (tmp.app_store?.shared_secret) {
      tmp.app_store.shared_secret = security.encrypt(tmp.app_store.shared_secret);
    }

    return tmp;
  }

  public static decryptSettings(settings: PaymentMethodSettings) {
    const tmp = cloneDeep(settings);
    if (tmp.stripe) {
      tmp.stripe.secret_key = security.decrypt(tmp.stripe.secret_key);
      tmp.stripe.webhook_signing_secret = security.decrypt(tmp.stripe.webhook_signing_secret);
    }
    if (tmp.google_play) {
      tmp.google_play.service_account_json = security.decrypt(tmp.google_play.service_account_json);
    }
    if (tmp.app_store?.private_key_pem) {
      tmp.app_store.private_key_pem = security.decrypt(tmp.app_store.private_key_pem);
    }
    if (tmp.app_store?.shared_secret) {
      tmp.app_store.shared_secret = security.decrypt(tmp.app_store.shared_secret);
    }

    return tmp;
  }

  getStripeClient() {
    if (this.type !== 'stripe') {
      throw new Error('payment method is not stripe');
    }
    if (!this.settings.stripe) {
      throw new Error('payment method config insufficient for stripe');
    }

    const { secret_key: secretKey } = PaymentMethod.decryptSettings(this.settings).stripe || {};
    if (!secretKey) {
      throw new Error('stripe secret key is required');
    }
    const clientKey = `${this.id}:${secretKey}`;
    const existingClient = stripeClients.get(clientKey) as Stripe;

    if (existingClient) {
      return existingClient;
    }

    try {
      stripeClients.delete(this.id);
      const client = new Stripe(secretKey, { apiVersion: STRIPE_API_VERSION });
      stripeClients.set(clientKey, client);
      return client as Stripe;
    } catch (e) {
      return {} as Stripe;
    }
  }

  getOcapClient() {
    if (this.type !== 'arcblock') {
      throw new Error('payment method is not arcblock');
    }
    if (!this.settings.arcblock) {
      throw new Error('payment method config insufficient to create ocap client');
    }

    const host = this.settings.arcblock?.api_host;
    const cached = ocapClients.has(host);
    if (!cached) {
      // 1.30.9: the default `@ocap/client` entry went CBOR-only, but current ABT
      // wallets only decode protobuf-encoded tx bytes. Use `@ocap/client/legacy`
      // (protobuf client) so encode / decode / broadcast all stay protobuf,
      // matching the wallet signatures and what the chain node expects to hash.
      const created = new OcapClient(host);
      ocapClients.set(host, created);
      return created;
    }

    return ocapClients.get(host) as OcapClient;
  }

  getEvmClient() {
    if (!EVM_CHAIN_TYPES.includes(this.type)) {
      throw new Error(`payment method ${this.type} is not EVM compatible`);
    }

    if (!this.settings[this.type as keyof PaymentMethodSettings]) {
      throw new Error(`payment method config insufficient for ${this.type}`);
    }

    if (evmClients.has(this.id)) {
      return evmClients.get(this.id) as JsonRpcProvider;
    }

    const settings = PaymentMethod.decryptSettings(this.settings);
    // @ts-ignore
    const client = new ethers.JsonRpcProvider(settings[this.type as keyof PaymentMethodSettings]?.api_host);
    evmClients.set(this.id, client);

    return client as JsonRpcProvider;
  }

  getGooglePlayClient() {
    if (this.type !== 'google_play') {
      throw new Error('payment method is not google_play');
    }
    if (!this.settings.google_play) {
      throw new Error('payment method config insufficient for google_play');
    }

    if (googlePlayClients.has(this.id)) {
      return googlePlayClients.get(this.id) as GooglePlayClient;
    }

    const settings = PaymentMethod.decryptSettings(this.settings);
    const client = GooglePlayClient.fromSettings(settings.google_play!);
    googlePlayClients.set(this.id, client);
    return client;
  }

  getAppStoreClient() {
    if (this.type !== 'app_store') {
      throw new Error('payment method is not app_store');
    }
    if (!this.settings.app_store) {
      throw new Error('payment method config insufficient for app_store');
    }

    if (appStoreClients.has(this.id)) {
      return appStoreClients.get(this.id) as AppStoreClient;
    }

    const settings = PaymentMethod.decryptSettings(this.settings);
    const client = AppStoreClient.fromSettings(settings.app_store!);
    appStoreClients.set(this.id, client);
    return client;
  }

  public static async supportAutoCharge(id: string) {
    const method = await PaymentMethod.findByPk(id);
    return method && CHARGE_SUPPORTED_CHAIN_TYPES.includes(method.type);
  }
}

export type TPaymentMethod = InferAttributes<PaymentMethod>;
