/* 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, PaymentDetails, PaymentError } from './types';

export const nextPayoutId = createIdGenerator('po', 24);

// @link https://stripe.com/docs/api/payouts
export class Payout extends Model<InferAttributes<Payout>, InferCreationAttributes<Payout>> {
  // Unique identifier for the object.
  declare id: CreationOptional<string>;

  declare livemode: boolean;
  declare automatic: boolean;
  declare description: string;
  declare destination: string;

  declare amount: string;
  declare payment_details?: PaymentDetails;

  // relations
  declare currency_id: string;
  declare customer_id: string;
  declare payment_intent_id: string;
  declare payment_method_id: string;

  declare metadata?: Record<string, any>;

  declare vendor_info?: {
    vendor_id: string;
    app_pid?: string;
    order_id: string;
    commission_amount: string;
  };

  declare status: LiteralUnion<
    'pending' | 'paid' | 'failed' | 'canceled' | 'in_transit' | 'deferred' | 'reverted',
    string
  >;

  // retry logic
  declare failure_message?: string;
  declare failure_code?: LiteralUnion<
    | 'account_closed'
    | 'account_frozen'
    | 'bank_account_restricted'
    | 'bank_ownership_changed'
    | 'could_not_process'
    | 'debit_not_authorized'
    | 'declined'
    | 'incorrect_account_holder_address'
    | 'incorrect_account_holder_name'
    | 'incorrect_account_holder_tax_id'
    | 'incorrect_account_type'
    | 'insufficient_funds'
    | 'invalid_account_number',
    string
  >;
  declare attempt_count: number;
  declare attempted: boolean;
  declare next_attempt?: number;
  declare last_attempt_error?: PaymentError | null;

  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: nextPayoutId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    automatic: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    description: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    amount: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    destination: {
      type: DataTypes.STRING(512),
      allowNull: false,
    },
    payment_details: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    currency_id: {
      type: DataTypes.STRING(16),
      allowNull: false,
    },
    customer_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    payment_intent_id: {
      type: DataTypes.STRING(16),
      allowNull: false,
    },
    payment_method_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    status: {
      type: DataTypes.ENUM('paid', 'pending', 'failed', 'canceled', 'in_transit', 'deferred', 'reverted'),
      allowNull: false,
    },
    failure_message: {
      type: DataTypes.STRING(256),
      allowNull: true,
    },
    failure_code: {
      type: DataTypes.ENUM(
        'account_closed',
        'account_frozen',
        'bank_account_restricted',
        'bank_ownership_changed',
        'could_not_process',
        'debit_not_authorized',
        'declined',
        'incorrect_account_holder_address',
        'incorrect_account_holder_name',
        'incorrect_account_holder_tax_id',
        'incorrect_account_type',
        'insufficient_funds',
        'invalid_account_number'
      ),
      allowNull: true,
    },
    attempt_count: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
    },
    attempted: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    next_attempt: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    last_attempt_error: {
      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,
    },
  };

  public static initialize(sequelize: any) {
    this.init(
      {
        ...Payout.GENESIS_ATTRIBUTES,
        vendor_info: {
          type: DataTypes.JSON,
          allowNull: true,
        },
      },
      {
        sequelize,
        modelName: 'Payout',
        tableName: 'payouts',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Payout, options) => {
            createEvent('Payout', 'payout.created', model, options).catch(console.error);
            if (model.status === 'paid') {
              createEvent('Payout', 'payout.paid', model, options).catch(console.error);
            }
          },
          afterUpdate: (model: Payout, options) => {
            createStatusEvent(
              'Payout',
              'payout',
              { canceled: 'canceled', failed: 'failed', paid: 'paid' },
              model,
              options
            ).catch(console.error);
          },
        },
      }
    );
  }

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

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

  public static async getPayoutAmountByCustomer(customerId: string, status: string = 'succeeded'): Promise<GroupedBN> {
    const payouts = await Payout.findAll({
      where: {
        status,
        customer_id: customerId,
        amount: { [Op.gt]: '0' },
      },
    });

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

      acc[key] = new BN(acc[key]).add(new BN(payout.amount)).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);
  }

  public static async getPayoutLockedAmount(where?: any) {
    const payouts = await Payout.findAll({
      where: {
        status: {
          [Op.in]: ['pending', 'in_transit'],
        },
        ...(where || {}),
      },
      attributes: ['amount'],
    });

    return payouts.reduce((acc: GroupedBN, payout) => {
      const key = payout.currency_id;
      if (!acc[key]) {
        acc[key] = '0';
      }
      acc[key] = new BN(acc[key]).add(new BN(payout.amount)).toString();
      return acc;
    }, {});
  }
}

export type TPayout = InferAttributes<Payout>;
