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

import { createEvent } from '../../libs/audit';
import { createIdGenerator } from '../../libs/util';

export const nextProductId = createIdGenerator('prod', 14);

export type ProductFeature = { name: string };

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

  // Whether the product is currently available for purchase.
  declare active: boolean;
  declare livemode: boolean;
  declare locked: CreationOptional<boolean>;

  declare type: LiteralUnion<'service' | 'good' | 'credit', string>;

  // The product’s name, meant to be displayable to the customer.
  declare name: string;

  // The product’s description, meant to be displayable to the customer.
  declare description: string;

  // A list of up to 8 URLs of images for this product, meant to be displayable to the customer.
  declare images: any[];

  // A list of up to 15 features for this product. These are displayed in pricing tables.
  declare features: ProductFeature[];

  // A label that represents units of this product.
  declare unit_label?: string;

  // The ID of the Price object that is the default price for this product.
  declare default_price_id?: string;

  declare metadata?: Record<string, any>;

  declare statement_descriptor?: string;

  // If set, we will mint an nft to consumer on purchase
  declare nft_factory?: string;

  // Tax code for this product (e.g., txcd_10103000)
  declare tax_code?: string;

  declare cross_sell?: {
    cross_sells_to_id: string;
  };

  declare vendor_config?: Array<{
    vendor_id: string;
    vendor_key: string;
    vendor_type: string;
    name?: string;
    description?: string;
    commission_rate?: number;
    commission_type?: 'percentage' | 'fixed_amount';
    amount?: string;
    commissionAmount?: string;
    custom_params?: Record<string, any>;
  }>;

  // TODO: goods related props are not supported
  // declare package_dimensions?: any;
  // declare shippable?: boolean;

  declare created_at: CreationOptional<Date>;
  declare created_via: LiteralUnion<'api' | 'dashboard' | 'portal' | 'donation', string>;
  declare updated_at: CreationOptional<Date>;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(18),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextProductId,
    },
    active: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    locked: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    type: {
      type: DataTypes.ENUM('service', 'good', 'credit'),
    },
    name: {
      type: DataTypes.STRING(512),
    },
    description: {
      type: DataTypes.STRING(2048),
    },
    images: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    features: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    unit_label: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    default_price_id: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    statement_descriptor: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    nft_factory: {
      type: DataTypes.STRING(40),
      allowNull: true,
    },
    tax_code: {
      type: DataTypes.STRING(20),
      allowNull: true,
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    created_via: {
      type: DataTypes.ENUM('api', 'dashboard', 'portal', 'donation'),
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  public static initialize(sequelize: any) {
    this.init(
      {
        ...Product.GENESIS_ATTRIBUTES,
        cross_sell: {
          type: DataTypes.JSON,
          allowNull: true,
        },
        vendor_config: {
          type: DataTypes.JSON,
          allowNull: true,
        },
      },
      {
        sequelize,
        modelName: 'Product',
        tableName: 'products',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Product, options) => {
            createEvent('Product', 'product.created', model, options).catch(console.error);
          },
          afterUpdate: (model: Product, options) => {
            createEvent('Product', 'product.updated', model, options).catch(console.error);
          },
          afterDestroy: (model: Product, options) => {
            createEvent('Product', 'product.deleted', model, options).catch(console.error);
          },
        },
      }
    );
  }

  public static associate(models: any) {
    this.hasMany(models.Price, {
      foreignKey: 'product_id',
      as: 'prices',
    });
    this.hasOne(models.Price, {
      sourceKey: 'default_price_id',
      foreignKey: 'id',
      as: 'default_price',
    });
  }

  public static async expand(id: string) {
    // @ts-ignore
    const { Price, PaymentCurrency } = this.sequelize.models;
    const product = await Product.findOne({
      where: { id },
      include: [
        { model: Price, as: 'prices', include: [{ model: PaymentCurrency, as: 'currency' }] },
        { model: Price, as: 'default_price' },
      ],
    });

    if (product) {
      const doc = product.toJSON();
      // @ts-ignore
      const currencies = await PaymentCurrency.findAll();
      // @ts-ignore
      for (const price of doc.prices) {
        if (Array.isArray(price.currency_options)) {
          price.currency_options.forEach((x: any) => {
            x.currency = currencies.find((c: any) => c.id === x.currency_id);
          });
          const exist = price.currency_options.find((x: any) => x.currency_id === price.currency_id);
          if (!exist) {
            price.currency_options.unshift(
              pick(price, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount', 'currency'])
            );
          }
        } else {
          price.currency_options = [
            pick(price, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount', 'currency']),
          ];
        }
      }

      if (doc.cross_sell?.cross_sells_to_id) {
        // @ts-ignore
        doc.cross_sell.cross_sells_to = await Product.expand(doc.cross_sell.cross_sells_to_id);
      }

      return doc;
    }

    return null;
  }

  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 TProduct = InferAttributes<Product>;
