/* 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 nextRefundId = createIdGenerator('re', 24);

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

  declare livemode: boolean;
  declare description: 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 invoice_id?: string;
  declare subscription_id?: string;

  declare metadata?: Record<string, any>;

  // processing: refund transfer succeeded, waiting for credit burn (for onchain credit only)
  declare status: LiteralUnion<
    'pending' | 'requires_action' | 'processing' | 'failed' | 'canceled' | 'succeeded',
    string
  >;
  declare reason?: LiteralUnion<
    'duplicate' | 'requested_by_customer' | 'requested_by_admin' | 'fraudulent' | 'expired_uncaptured_charge',
    string
  >;
  declare failure_reason?: LiteralUnion<
    | 'lost_or_stolen_card'
    | 'expired_or_canceled_card'
    | 'charge_for_pending_refund_disputed'
    | 'insufficient_funds'
    | 'declined'
    | 'merchant_request'
    | 'unknown',
    string
  >;

  // retry logic
  declare attempt_count: number;
  declare attempted: boolean;
  declare next_attempt?: number;
  declare last_attempt_error?: PaymentError | null;

  // credit impacts for this refund
  declare starting_balance: string; // usd credit
  declare ending_balance: string; // usd credit
  declare starting_token_balance?: Record<string, string>; // token credits
  declare ending_token_balance?: Record<string, string>; // token credits

  declare created_at: CreationOptional<Date>;
  declare updated_at: CreationOptional<Date>;
  declare type: LiteralUnion<'refund' | 'stake_return', string>;

  // Refund origin (added for IAP support).
  //   merchant_initiated —商户主动调 API 退款 (Stripe / 链上)
  //   platform_initiated — Apple/Google webhook 推过来的退款，我们被动接收
  declare source?: LiteralUnion<'merchant_initiated' | 'platform_initiated', string>;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(30),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextRefundId,
    },
    description: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    amount: {
      type: DataTypes.STRING(32),
      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,
    },
    invoice_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    subscription_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    status: {
      type: DataTypes.ENUM('requires_action', 'pending', 'failed', 'canceled', 'succeeded'),
      allowNull: false,
    },
    reason: {
      type: DataTypes.ENUM('duplicate', 'requested_by_customer', 'requested_by_admin', 'fraudulent'),
      allowNull: true,
    },
    failure_reason: {
      type: DataTypes.ENUM(
        'lost_or_stolen_card',
        'expired_or_canceled_card',
        'charge_for_pending_refund_disputed',
        'insufficient_funds',
        'declined',
        'merchant_request',
        'unknown'
      ),
      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,
    },
    starting_balance: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    ending_balance: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    starting_token_balance: {
      type: DataTypes.JSON,
      defaultValue: {},
    },
    ending_token_balance: {
      type: DataTypes.JSON,
      defaultValue: {},
    },
    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(
      {
        ...Refund.GENESIS_ATTRIBUTES,
        payment_method_id: {
          type: DataTypes.STRING(30),
          allowNull: true,
        },
        type: {
          type: DataTypes.ENUM('refund', 'stake_return'),
          allowNull: true,
          defaultValue: 'refund',
        },
        source: {
          type: DataTypes.STRING(30),
          allowNull: true,
          defaultValue: 'merchant_initiated',
        },
      },
      {
        sequelize,
        modelName: 'Refund',
        tableName: 'refunds',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Refund, options) => {
            createEvent('Refund', 'refund.created', model, options).catch(console.error);
          },
          afterUpdate: (model: Refund, options) => {
            createStatusEvent(
              'Refund',
              'refund',
              { canceled: 'canceled', processing: 'processing', succeeded: 'succeeded' },
              model,
              options
            ).catch(console.error);
          },
          afterDestroy: (model: Refund, options) => {
            createEvent('Refund', 'refund.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.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',
    });
    this.hasOne(models.Invoice, {
      sourceKey: 'invoice_id',
      foreignKey: 'id',
      as: 'invoice',
    });
    this.hasOne(models.Subscription, {
      sourceKey: 'subscription_id',
      foreignKey: 'id',
      as: 'subscription',
    });
  }

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

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

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

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

export type TRefund = InferAttributes<Refund>;
