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

import { createCustomEvent, createEvent, createStatusEvent } from '../../libs/audit';
import logger from '../../libs/logger';
import { createIdGenerator } from '../../libs/util';
import { Invoice } from './invoice';
import type {
  CreditScheduleState,
  PaymentDetails,
  PaymentSettings,
  PriceRecurring,
  ServiceAction,
  SubscriptionBillingThresholds,
} from './types';

export const nextSubscriptionId = createIdGenerator('sub', 24);

// FIXME: subscription settings https://dashboard.stripe.com/settings/billing/automatic
// eslint-disable-next-line prettier/prettier
export class Subscription extends Model<InferAttributes<Subscription>, InferCreationAttributes<Subscription>> {
  declare id: CreationOptional<string>;

  declare livemode: boolean;

  declare currency_id: string;
  declare customer_id: string;

  declare current_period_end: number;
  declare current_period_start: number;

  declare default_payment_method_id: string;
  declare description?: string;
  declare latest_invoice_id?: string;

  declare metadata: Record<string, any>;

  declare pending_setup_intent?: string;
  declare pending_update?: {
    billing_cycle_anchor?: number;
    expires_at?: number;
    subscription_items?: any[];
    trial_end?: number;
    [key: string]: any;
  };

  declare status: LiteralUnion<
    'active' | 'past_due' | 'paused' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'trialing',
    string
  >;

  declare cancel_at_period_end: boolean;
  declare cancel_at: CreationOptional<number>;
  declare canceled_at: CreationOptional<number>;
  declare cancelation_details?: {
    comment: string;
    feedback: LiteralUnion<
      | 'too_expensive'
      | 'missing_features'
      | 'switched_service'
      | 'unused'
      | 'customer_service'
      | 'too_complex'
      | 'low_quality'
      | 'other',
      string
    >;
    reason: LiteralUnion<'cancellation_requested' | 'payment_disputed' | 'payment_failed' | 'stake_revoked', string>;
    return_stake?: boolean;
    slash_stake?: boolean;
    slash_reason?: string;
    refund?: LiteralUnion<'last' | 'proration' | 'none', string>;
    requested_by?: string;
  };

  declare billing_cycle_anchor: number;
  declare billing_thresholds?: SubscriptionBillingThresholds;

  declare collection_method: LiteralUnion<'charge_automatically' | 'send_invoice', string>;

  // Number of days a customer has to pay invoices generated by this subscription.
  declare days_until_due?: number;

  // Number of days a subscription is canceled after past_due
  declare days_until_cancel?: number;

  declare discount_id?: string;

  declare next_pending_invoice_item_invoice?: number;

  declare pause_collection?: {
    behavior: LiteralUnion<'keep_as_draft' | 'mark_uncollectible' | 'void', string>;
    resumes_at: number;
  };

  declare payment_settings?: PaymentSettings;

  declare pending_invoice_item_interval: PriceRecurring;

  declare schedule_id?: string;

  // If the subscription has ended, the date the subscription ended.
  declare ended_at?: number;
  declare start_date: number;
  declare trial_end?: number;
  declare trial_start?: number;
  declare trial_settings?: {
    end_behavior: {
      missing_payment_method: LiteralUnion<'cancel' | 'pause' | 'create_invoice', string>;
    };
  };

  // 3rd party payment tx hash
  declare payment_details?: PaymentDetails;
  declare slippage_config?: {
    mode?: 'percent' | 'rate';
    percent?: number;
    min_acceptable_rate?: string;
    base_currency?: string;
    updated_at_ms?: number;
  } | null;

  declare proration_behavior?: LiteralUnion<'always_invoice' | 'create_prorations' | 'none', string>;
  declare payment_behavior?: LiteralUnion<'allow_incomplete' | 'error_if_incomplete' | 'pending_if_incomplete', string>;

  declare service_actions?: ServiceAction[];

  declare recovered_from?: string;

  // TODO: following fields not supported
  // application
  // application_fee_percent
  // automatic_tax
  // default_tax_rates
  // on_behalf_of
  // transfer_data

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

  declare overdraft_protection?: {
    enabled: boolean;
    payment_method_id: string;
    payment_details: PaymentDetails;
  };

  // Credit schedule state for each price with schedule-based delivery
  declare credit_schedule_state?: CreditScheduleState;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(30),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextSubscriptionId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    currency_id: {
      type: DataTypes.STRING(15),
      allowNull: false,
    },
    customer_id: {
      type: DataTypes.STRING(18),
      allowNull: false,
    },
    cancel_at_period_end: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    current_period_end: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    current_period_start: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    default_payment_method_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    description: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    latest_invoice_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    pending_setup_intent: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    pending_update: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    status: {
      type: DataTypes.ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'trialing', 'paused'),
      allowNull: false,
    },
    // 将来在什么时候这个订阅会被取消
    cancel_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    // 什么时候开始说要去取消订阅的
    canceled_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    cancelation_details: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    billing_cycle_anchor: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    billing_thresholds: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    collection_method: {
      type: DataTypes.ENUM('charge_automatically', 'send_invoice'),
      allowNull: false,
    },
    slippage_config: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    days_until_due: {
      type: DataTypes.NUMBER,
      allowNull: true,
    },
    discount_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    next_pending_invoice_item_invoice_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    pause_collection: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    payment_settings: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    pending_invoice_item_interval: {
      type: DataTypes.JSON,
      allowNull: false,
    },
    schedule_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    end_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    start_date: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    payment_details: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    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,
    },
  };

  public static initialize(sequelize: any) {
    this.init(
      {
        ...Subscription.GENESIS_ATTRIBUTES,
        proration_behavior: {
          type: DataTypes.ENUM('always_invoice', 'create_prorations', 'none'),
          defaultValue: 'none',
        },
        payment_behavior: {
          type: DataTypes.ENUM(
            'default_incomplete',
            'allow_incomplete',
            'error_if_incomplete',
            'pending_if_incomplete'
          ),
          defaultValue: 'default_incomplete',
        },
        days_until_cancel: {
          type: DataTypes.NUMBER,
          allowNull: true,
        },
        service_actions: {
          type: DataTypes.JSON,
          allowNull: true,
          defaultValue: [],
        },
        trial_end: {
          type: DataTypes.INTEGER,
          allowNull: true,
        },
        trial_start: {
          type: DataTypes.INTEGER,
          allowNull: true,
        },
        trial_settings: {
          type: DataTypes.JSON,
          allowNull: true,
        },
        recovered_from: {
          type: DataTypes.STRING(40),
          allowNull: true,
        },
        overdraft_protection: {
          type: DataTypes.JSON,
          allowNull: true,
          defaultValue: {
            enabled: false,
            payment_method_id: null,
            payment_details: null,
          },
        },
        credit_schedule_state: {
          type: DataTypes.JSON,
          allowNull: true,
          defaultValue: null,
        },
      },
      {
        sequelize,
        modelName: 'Subscription',
        tableName: 'subscriptions',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Subscription, options) => {
            createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error);
          },
          afterUpdate: async (model: Subscription, options) => {
            createEvent('Subscription', 'customer.subscription.updated', model, options).catch(console.error);

            if (model.trial_start) {
              const previousLatestInvoiceId = model.previous('latest_invoice_id');

              if (
                previousLatestInvoiceId &&
                model.latest_invoice_id &&
                previousLatestInvoiceId !== model.latest_invoice_id
              ) {
                const count: number = await Invoice.count({
                  where: {
                    subscription_id: model.id,
                  },
                });
                if (count === 2) {
                  // 当且仅当 有试用期的订阅更新了 && 恰好有 2 次账单,此时订阅的试用期刚好结束
                  createEvent('Subscription', 'customer.subscription.trial_end', model, options).catch(console.error);
                }
              }
            }

            createStatusEvent(
              'Subscription',
              'customer.subscription',
              { canceled: 'deleted', past_due: 'past_due' },
              model,
              options
            ).catch(console.error);
            createCustomEvent('Subscription', 'customer.subscription', getSubscriptionEventType, 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: 'default_payment_method_id',
      foreignKey: 'id',
      as: 'paymentMethod',
    });
    this.hasOne(models.Customer, {
      sourceKey: 'customer_id',
      foreignKey: 'id',
      as: 'customer',
    });
    this.hasMany(models.SubscriptionItem, {
      foreignKey: 'subscription_id',
      as: 'items',
    });
  }

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

  public isActive() {
    return ['active', 'trialing'].includes(this.status);
  }

  public isScheduledToCancel() {
    return this.isActive() && (!!this.cancel_at_period_end || !!this.cancel_at);
  }

  public async isConsumesCredit(): Promise<boolean> {
    // @ts-ignore
    const { SubscriptionItem, Price } = this.sequelize.models;
    if (!SubscriptionItem || !Price) {
      return false;
    }

    // 查询订阅的所有价格项
    const items = await SubscriptionItem.findAll({
      where: { subscription_id: this.id },
      include: [
        {
          model: Price,
          as: 'price',
        },
      ],
    });

    return items.some((item: any) => {
      const recurring = item.price?.recurring;
      return recurring && recurring.usage_type === 'metered' && recurring.meter_id;
    });
  }

  public async start() {
    if (this.status === 'active') {
      logger.warn(`subscription already active: ${this.id}`);
      return false;
    }

    if (this.isImmutable()) {
      logger.warn(`subscription is immutable: ${this.id}`);
      return false;
    }

    if (this.trial_end && this.trial_end > Date.now() / 1000) {
      if (this.status !== 'trialing') {
        await this.update({ status: 'trialing' });
        createEvent('Subscription', 'customer.subscription.trial_start', this).catch(console.error);
      }
    } else {
      await this.update({ status: 'active' });
      createEvent('Subscription', 'customer.subscription.started', this).catch(console.error);
    }

    return true;
  }

  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 getToSubscriptions(subscriptionId: string): Promise<string[]> {
    const subscriptions = [];
    let cursor = subscriptionId;
    do {
      // eslint-disable-next-line no-await-in-loop
      const from = await this.findOne({ where: { recovered_from: cursor } });
      if (from) {
        cursor = from.id;
        subscriptions.push(cursor);
      } else {
        cursor = '';
      }
    } while (cursor);

    return subscriptions;
  }

  public static async getFromSubscriptions(subscriptionId: string): Promise<string[]> {
    const subscriptions = [];
    let cursor = subscriptionId;
    do {
      // eslint-disable-next-line no-await-in-loop
      const to = await this.findOne({ where: { id: cursor } });
      if (to?.recovered_from) {
        cursor = to.recovered_from;
        subscriptions.push(cursor);
      } else {
        cursor = '';
      }
    } while (cursor);

    return subscriptions;
  }
}

export type TSubscription = InferAttributes<Subscription>;

export function getSubscriptionEventType(current: TSubscription, previous: Partial<TSubscription>) {
  if (current.pause_collection && !previous.pause_collection) {
    return 'paused';
  }
  if (!current.pause_collection && previous.pause_collection) {
    return 'resumed';
  }

  return '';
}
