/* eslint-disable @typescript-eslint/lines-between-class-members */
/* eslint-disable @typescript-eslint/indent */
import { BN } from '@ocap/util';
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Op, Sequelize } from 'sequelize';
import type { LiteralUnion } from 'type-fest';

import { createEvent, createStatusEvent } from '../../libs/audit';
import { createIdGenerator } from '../../libs/util';
import type { GroupedBN, PaymentBeneficiary, PaymentDetails, PaymentError } from './types';

export const nextPaymentIntentId = createIdGenerator('pi', 24);

// @link https://stripe.com/docs/api/payment_intents
// eslint-disable-next-line prettier/prettier
export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCreationAttributes<PaymentIntent>> {
  // Unique identifier for the object.
  declare id: CreationOptional<string>;

  declare livemode: boolean;
  declare description: string;

  declare amount: string;
  declare amount_received?: string;
  declare amount_capturable?: string;
  declare amount_details?: {
    tip: string;
  };

  // Payment currency id
  declare currency_id: string;
  // ID of the Customer this PaymentIntent belongs to, if one exists
  declare customer_id?: string;
  declare payment_method_id: string;
  declare invoice_id?: string;
  // The latest charge created by this payment intent.
  declare last_charge?: string;

  // The payment error encountered in the previous PaymentIntent confirmation
  declare last_payment_error: PaymentError | null;

  declare metadata?: Record<string, any>;

  declare receipt_email?: string;

  declare statement_descriptor: string;
  declare statement_descriptor_suffix?: string;

  declare status: LiteralUnion<
    | 'requires_payment_method'
    | 'requires_confirmation'
    | 'requires_action'
    | 'processing'
    | 'requires_capture'
    | 'canceled'
    | 'succeeded',
    string
  >;

  declare canceled_at?: number;
  declare cancellation_reason?: LiteralUnion<
    | 'abandoned'
    | 'duplicate'
    | 'requested_by_customer'
    | 'fraudulent'
    | 'failed_invoice'
    | 'void_invoice'
    | 'automatic',
    string
  >;

  declare capture_method: LiteralUnion<'automatic' | 'manual', string>;
  declare confirmation_method: LiteralUnion<'automatic' | 'manual', string>;

  declare payment_method_types: string[];

  declare review?: string;

  // 3rd party payment tx hash
  declare payment_details?: PaymentDetails;

  declare setup_future_usage?: LiteralUnion<'off_session' | 'on_session', string>;

  declare quote_locked_at?: Date;

  // how to split the payment from this link
  declare beneficiaries?: PaymentBeneficiary[];

  // TODO: following fields not supported yet
  // automatic_payment_methods
  // client_secret
  // next_action
  // shipping
  // application
  // application_fee_amount
  // on_behalf_of
  // payment_method_options
  // processing
  // transfer_data
  // transfer_group

  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: nextPaymentIntentId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    amount: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    amount_received: {
      type: DataTypes.STRING(32),
      default: '0',
    },
    amount_capturable: {
      type: DataTypes.STRING(32),
      default: '0',
    },
    amount_details: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    currency_id: {
      type: DataTypes.STRING(16),
      allowNull: false,
    },
    customer_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    description: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    last_payment_error: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    last_charge: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    invoice_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    payment_method_id: {
      type: DataTypes.STRING(30),
      allowNull: false,
    },
    receipt_email: {
      type: DataTypes.STRING(255),
      allowNull: true,
    },
    statement_descriptor: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    statement_descriptor_suffix: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    status: {
      type: DataTypes.ENUM(
        'requires_payment_method',
        'requires_confirmation',
        'requires_action',
        'processing',
        'requires_capture',
        'canceled',
        'succeeded'
      ),
      allowNull: false,
    },
    canceled_at: {
      type: DataTypes.DATE,
      allowNull: true,
    },
    cancellation_reason: {
      type: DataTypes.ENUM(
        'abandoned',
        'duplicate',
        'requested_by_customer',
        'fraudulent',
        'failed_invoice',
        'void_invoice',
        'automatic'
      ),
      allowNull: true,
    },
    capture_method: {
      type: DataTypes.ENUM('automatic', 'manual'),
      allowNull: false,
    },
    confirmation_method: {
      type: DataTypes.ENUM('automatic', 'manual'),
      allowNull: false,
    },
    payment_method_types: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    review: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    payment_details: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    setup_future_usage: {
      type: DataTypes.ENUM('on_session', 'off_session'),
      allowNull: true,
    },
    quote_locked_at: {
      type: DataTypes.DATE,
      allowNull: true,
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  public static initialize(sequelize: any) {
    this.init(
      {
        ...PaymentIntent.GENESIS_ATTRIBUTES,
        beneficiaries: {
          type: DataTypes.JSON,
          allowNull: true,
          defaultValue: [],
        },
      },
      {
        sequelize,
        modelName: 'PaymentIntent',
        tableName: 'payment_intents',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: PaymentIntent, options) => {
            createEvent('PaymentIntent', 'payment_intent.created', model, options).catch(console.error);
          },
          afterUpdate: (model: PaymentIntent, options) => {
            createStatusEvent(
              'PaymentIntent',
              'payment_intent',
              {
                requires_action: 'requires_action',
                processing: 'processing',
                canceled: 'canceled',
                succeeded: 'succeeded',
              },
              model,
              options
            ).catch(console.error);
          },
          afterDestroy: (model: PaymentIntent, options) => {
            createEvent('PaymentIntent', 'payment_intent.deleted', model, options).catch(console.error);
          },
        },
      }
    );
  }

  public static associate(models: any) {
    this.hasOne(models.PaymentCurrency, {
      sourceKey: 'currency_id',
      foreignKey: 'id',
      as: 'paymentCurrency',
    });
    this.hasOne(models.PaymentMethod, {
      sourceKey: 'payment_method_id',
      foreignKey: 'id',
      as: 'paymentMethod',
    });
    this.hasOne(models.Customer, {
      sourceKey: 'customer_id',
      foreignKey: 'id',
      as: 'customer',
    });
  }

  public isImmutable() {
    return ['canceled', 'succeeded'].includes(this.status);
  }

  public static async getPaidAmountByCustomer(customerId: string): Promise<GroupedBN> {
    const payments = await PaymentIntent.findAll({
      where: {
        status: 'succeeded',
        customer_id: customerId,
        amount_received: { [Op.gt]: '0' },
      },
    });

    return payments.reduce((acc: GroupedBN, payment) => {
      const key = payment.currency_id;
      if (!acc[key]) {
        acc[key] = '0';
      }

      acc[key] = new BN(acc[key]).add(new BN(payment.amount_received)).toString();

      return acc;
    }, {});
  }

  public static getSummary(livemode: boolean = true, field: string = 'status') {
    return this.findAndCountAll({
      where: { livemode },
      attributes: [field, [Sequelize.fn('COUNT', Sequelize.col(field)), 'count']],
      group: [field],
      raw: true,
    }).then((res) => res.rows);
  }
}

export type TPaymentIntent = InferAttributes<PaymentIntent>;
