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

import { createEvent, createStatusEvent } from '../../libs/audit';
import { createIdGenerator } from '../../libs/util';
import type {
  CustomerAddress,
  CustomerShipping,
  DiscountAmount,
  GroupedBN,
  GroupedStrList,
  PaymentError,
  PaymentSettings,
  SimpleCustomField,
} from './types';

export const nextInvoiceId = createIdGenerator('in', 24);

// eslint-disable-next-line prettier/prettier
export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttributes<Invoice>> {
  declare id: CreationOptional<string>;

  // A unique, identifying string that appears on emails sent to the customer for this invoice
  declare number: string;

  declare livemode: boolean;
  declare description?: string;
  declare statement_descriptor: string;
  declare period_end: number;
  declare period_start: number;

  declare auto_advance: boolean;
  // Whether payment was successfully collected for this invoice
  declare paid: boolean;
  // Returns true if the invoice was manually marked paid
  declare paid_out_of_band: boolean;

  declare status: LiteralUnion<'draft' | 'open' | 'void' | 'paid' | 'uncollectible', string>;
  declare collection_method: LiteralUnion<'charge_automatically' | 'send_invoice', string>;
  declare billing_reason: LiteralUnion<
    | 'subscription_create'
    | 'subscription_cycle'
    | 'subscription_update'
    | 'subscription_recover'
    | 'subscription_threshold'
    | 'subscription_cancel'
    | 'subscription'
    | 'manual'
    | 'upcoming'
    | 'slash_stake'
    | 'stake'
    | 'overdraft_protection'
    | 'stake_overdraft_protection'
    | 'recharge'
    | 'auto_recharge',
    string
  >;

  declare currency_id: string;
  declare customer_id: string;
  declare payment_intent_id?: string;
  declare subscription_id?: string;
  declare checkout_session_id?: string;
  declare quote_id?: string;
  declare charge_id?: string;

  declare subscription_details?: any;
  declare subscription_proration_date?: number;

  declare subtotal: string;
  declare subtotal_excluding_tax: string;
  declare tax: string;
  declare total: string;

  declare amount_due: string; // total - amount_paid
  declare amount_paid: string;
  declare amount_remaining: string; // amount_due - amount_paid
  declare amount_shipping: string;

  declare starting_balance: string; // usd credit
  declare ending_balance: string; // usd credit
  declare starting_token_balance?: Record<string, string>; // token balances
  declare ending_token_balance?: Record<string, string>; // token balances

  declare attempt_count: number;
  declare attempted: boolean;
  declare next_payment_attempt?: number;

  declare custom_fields?: SimpleCustomField[];
  declare customer_address?: CustomerAddress;
  declare customer_email?: string;
  declare customer_name?: string;
  declare customer_phone?: string;
  declare customer_shipping?: CustomerShipping;

  declare discounts?: string[];
  declare total_discount_amounts?: DiscountAmount[];

  declare due_date?: number;
  declare effective_at?: number;

  declare footer?: string;

  declare from_invoice?: any;

  declare account_country?: string;
  declare account_name?: string;
  declare hosted_invoice_url?: string;

  // The link to download the PDF for the invoice
  declare invoice_pdf?: string;
  declare last_finalization_error?: PaymentError;
  declare latest_revision?: string;

  declare payment_settings?: PaymentSettings;
  declare default_payment_method_id: string;

  declare post_payment_credit_notes_amount?: string;
  declare pre_payment_credit_notes_amount?: string;

  declare rendering_options?: any;

  declare status_transitions?: {
    finalized_at?: number;
    marked_uncollectible_at?: number;
    paid_at?: number;
    voided_at?: number;
  };

  declare test_clock_id?: string;

  declare threshold_reason?: any;

  declare webhooks_delivered_at?: number;

  // This is the transaction number that appears on email receipts sent for this invoice.
  declare receipt_number?: string;

  declare metadata?: Record<string, any>;

  // TODO: following fields not supported
  // account_tax_ids
  // application
  // application_fee_amount
  // automatic_tax
  // customer_tax_exempt
  // customer_tax_ids
  // default_tax_rates
  // on_behalf_of
  // shipping_cost
  // shipping_details
  // total_tax_amounts
  // transfer_data

  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: nextInvoiceId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    auto_advance: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    account_country: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    account_name: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    charge_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    collection_method: {
      type: DataTypes.ENUM('charge_automatically', 'send_invoice'),
      allowNull: false,
    },
    currency_id: {
      type: DataTypes.STRING(15),
      allowNull: false,
    },
    customer_id: {
      type: DataTypes.STRING(18),
      allowNull: false,
    },
    description: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    hosted_invoice_url: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    payment_intent_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    period_end: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
    },
    period_start: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
    },
    status: {
      type: DataTypes.ENUM('draft', 'open', 'void', 'paid', 'uncollectible'),
      allowNull: false,
    },
    checkout_session_id: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    subscription_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    subscription_details: {
      type: DataTypes.JSON,
      defaultValue: {},
    },
    subscription_proration_date: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    total: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    subtotal: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    subtotal_excluding_tax: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    tax: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    amount_due: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    amount_paid: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    amount_remaining: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    amount_shipping: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    attempt_count: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
    },
    attempted: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    next_payment_attempt: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    billing_reason: {
      type: DataTypes.ENUM(
        'subscription_create',
        'subscription_cycle',
        'subscription_update',
        'subscription_threshold',
        'subscription_cancel',
        'subscription',
        'manual',
        'upcoming',
        'slash_stake',
        'stake',
        'overdraft_protection',
        'stake_overdraft_protection',
        'recharge'
      ),
    },
    custom_fields: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    customer_address: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    customer_name: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    customer_email: {
      type: DataTypes.STRING(128),
      allowNull: true,
    },
    customer_phone: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    customer_shipping: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    default_payment_method_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    discounts: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    due_date: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    effective_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    ending_balance: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    footer: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    from_invoice_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    invoice_pdf: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    last_finalization_error: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    latest_revision: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    number: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    paid: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    paid_out_of_band: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    payment_settings: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    post_payment_credit_notes_amount: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    pre_payment_credit_notes_amount: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    quote_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    receipt_number: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    days_until_due: {
      type: DataTypes.NUMBER,
      allowNull: true,
    },
    rendering_options: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    starting_balance: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    statement_descriptor: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    status_transitions: {
      type: DataTypes.JSON,
      defaultValue: {},
    },
    test_clock_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    threshold_reason: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    total_discount_amounts: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    webhooks_delivered_at: {
      type: DataTypes.INTEGER,
      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(
      {
        ...Invoice.GENESIS_ATTRIBUTES,
        starting_token_balance: {
          type: DataTypes.JSON,
          defaultValue: {},
        },
        ending_token_balance: {
          type: DataTypes.JSON,
          defaultValue: {},
        },
      },
      {
        sequelize,
        modelName: 'Invoice',
        tableName: 'invoices',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Invoice, options) => {
            createEvent('Invoice', 'invoice.created', model, options).catch(console.error);
            if (model.status === 'paid') {
              createEvent('Invoice', 'invoice.paid', model, options).catch(console.error);
            }
          },
          afterUpdate: (model: Invoice, options) => {
            createEvent('Invoice', 'invoice.updated', model, options).catch(console.error);
            createStatusEvent(
              'Invoice',
              'invoice',
              { open: 'finalized', void: 'voided', paid: 'paid', uncollectible: 'marked_uncollectible' },
              model,
              options
            ).catch(console.error);
          },
          afterDestroy: (model: Invoice, options) => {
            createEvent('Invoice', 'invoice.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: 'default_payment_method_id',
      foreignKey: 'id',
      as: 'paymentMethod',
    });
    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.Subscription, {
      sourceKey: 'subscription_id',
      foreignKey: 'id',
      as: 'subscription',
    });
    this.hasOne(models.SubscriptionItem, {
      sourceKey: 'id',
      foreignKey: 'subscription_id',
      as: 'subscriptionItem',
    });
    this.hasMany(models.InvoiceItem, {
      sourceKey: 'id',
      foreignKey: 'invoice_id',
      as: 'lines',
    });
  }

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

  private static async _getUncollectibleAmount(
    where: WhereOptions<Invoice>
  ): Promise<[GroupedBN, GroupedStrList, Invoice[]]> {
    const invoices = await Invoice.findAll({ where });
    const summary: GroupedBN = {};
    const detail: GroupedStrList = {};

    invoices.forEach((invoice) => {
      const key = invoice.currency_id;
      if (!summary[key]) {
        summary[key] = '0';
      }
      if (!detail[key]) {
        detail[key] = [];
      }

      summary[key] = new BN(summary[key]).add(new BN(invoice.amount_remaining)).toString();
      detail[key]!.push(invoice.id);
    });

    return [summary, detail, invoices];
  }

  public static getUncollectibleAmount({
    customerId,
    subscriptionId,
    currencyId,
    excludedInvoiceId,
    livemode,
  }: {
    customerId?: string;
    subscriptionId?: string;
    currencyId?: string;
    excludedInvoiceId?: string;
    livemode?: boolean;
  }) {
    if (!customerId && !subscriptionId) {
      throw new Error('customerId or subscriptionId is required for getUncollectibleAmount');
    }
    const where: WhereOptions<Invoice> = {
      status: ['uncollectible'],
      amount_remaining: { [Op.gt]: '0' },
    };
    if (currencyId) {
      where.currency_id = currencyId;
    }
    if (customerId) {
      where.customer_id = customerId;
    }
    if (subscriptionId) {
      where.subscription_id = subscriptionId;
    }
    if (excludedInvoiceId) {
      where.id = { [Op.not]: excludedInvoiceId };
    }
    if (typeof livemode === 'boolean') {
      where.livemode = livemode;
    }

    return this._getUncollectibleAmount(where);
  }

  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 TInvoice = InferAttributes<Invoice>;
