import { fromTokenToUnit } from '@ocap/util';
import isEmpty from 'lodash/isEmpty';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import {
  CreationOptional,
  DataTypes,
  FindOptions,
  InferAttributes,
  InferCreationAttributes,
  Model,
  Op,
  QueryTypes,
} from 'sequelize';
import type { LiteralUnion } from 'type-fest';

import { createEvent } from '../../libs/audit';
import logger from '../../libs/logger';
import { createIdGenerator, formatMetadata } from '../../libs/util';
import { sequelize } from '../sequelize';
import type { TPaymentCurrency } from './payment-currency';
import type { CustomUnitAmount, LineItem, PriceCurrency, PriceRecurring, PriceTier, TransformQuantity } from './types';
import { Meter, type TMeter } from './meter';

export const nextPriceId = createIdGenerator('price', 24);

// Duplicate here
type TPriceExpanded = TPrice & {
  object: 'price';
  product: any;
  currency: TPaymentCurrency;
  upsell?: {
    upsells_to: TPriceExpanded;
    upsells_to_id: string;
  };
  meter?: TMeter;
};
type TLineItemExpanded = LineItem & { price: TPriceExpanded; upsell_price: TPriceExpanded };

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

  // the product id
  declare product_id: string;

  // A brief description of the price, hidden from customers.
  declare nickname: string | null;

  // Whether the price can be used for new purchases.
  declare active: boolean;
  declare livemode: boolean;
  declare locked: CreationOptional<boolean>;

  // One of one_time or recurring depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.
  declare type: LiteralUnion<'one_time' | 'recurring', string>;

  // Describes how to compute the price per period.
  declare billing_scheme: LiteralUnion<'per_unit' | 'tiered', string>;

  // The unit amount in big number to be charged, represented as a whole integer if possible
  declare unit_amount: string;

  // The recurring components of a price such as interval and usage_type.
  declare recurring: PriceRecurring | null;

  // Defines if the tiering price should be graduated or volume based.
  declare tiers_mode: LiteralUnion<'graduated' | 'volume', string> | null;

  // Each element represents a pricing tier. This parameter requires billing_scheme to be set to tiered
  declare tiers: PriceTier[] | null;

  // When set, provides configuration for the amount to be adjusted by the customer during Checkout Sessions and Payment Links.
  declare custom_unit_amount: CustomUnitAmount | null;

  // A lookup key used to retrieve prices dynamically from a static string
  declare lookup_key: string | null;

  // Set of key-value pairs that you can attach to an object.
  declare metadata: Record<string, any>;

  declare transform_quantity: TransformQuantity | null;

  // Payment currency id
  declare currency_id: string;
  declare currency_options: PriceCurrency[];

  declare upsell?: { upsells_to_id: string } | null;

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

  // Quantity available for purchase,  0 means no limit
  declare quantity_available: number;
  // Quantity has been sold
  declare quantity_sold: number;
  // Quantity limit per checkout, 0 means no limit
  declare quantity_limit_per_checkout: number;

  declare tax_behavior?: LiteralUnion<'inclusive' | 'exclusive', string>;

  // Dynamic pricing fields
  declare pricing_type: LiteralUnion<'fixed' | 'dynamic', string>;
  declare base_currency: string | null;
  declare base_amount: string | null;
  declare dynamic_pricing_config: { lock_duration?: number } | null;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(32),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextPriceId,
    },
    product_id: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    nickname: {
      type: DataTypes.STRING(512),
    },
    active: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    locked: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    type: {
      type: DataTypes.ENUM('one_time', 'recurring'),
      allowNull: false,
    },
    billing_scheme: {
      type: DataTypes.ENUM('per_unit', 'tiered'),
      allowNull: false,
    },
    unit_amount: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    recurring: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    tiers_mode: {
      type: DataTypes.ENUM('graduated', 'volume'),
      allowNull: true,
    },
    tiers: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    custom_unit_amount: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    lookup_key: {
      type: DataTypes.STRING(128),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    transform_quantity: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    currency_id: {
      type: DataTypes.STRING(16),
    },
    currency_options: {
      type: DataTypes.JSON,
      defaultValue: [],
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    created_via: {
      type: DataTypes.ENUM('api', 'dashboard', 'portal'),
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  // eslint-disable-next-line @typescript-eslint/no-shadow
  public static initialize(sequelize: any) {
    this.init(
      {
        ...Price.GENESIS_ATTRIBUTES,
        upsell: {
          type: DataTypes.JSON,
          allowNull: true,
        },
        quantity_available: {
          type: DataTypes.INTEGER,
          defaultValue: 0,
          allowNull: false,
        },
        quantity_sold: {
          type: DataTypes.INTEGER,
          defaultValue: 0,
          allowNull: false,
        },
        quantity_limit_per_checkout: {
          type: DataTypes.INTEGER,
          defaultValue: 0,
          allowNull: false,
        },
        tax_behavior: {
          type: DataTypes.ENUM('inclusive', 'exclusive'),
          allowNull: false,
          defaultValue: 'inclusive',
        },
        pricing_type: {
          type: DataTypes.ENUM('fixed', 'dynamic'),
          allowNull: false,
          defaultValue: 'fixed',
        },
        base_currency: {
          type: DataTypes.STRING(8),
          allowNull: true,
        },
        base_amount: {
          type: DataTypes.STRING(32),
          allowNull: true,
        },
        dynamic_pricing_config: {
          type: DataTypes.JSON,
          allowNull: true,
        },
      },
      {
        sequelize,
        modelName: 'Price',
        tableName: 'prices',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Price, options) => {
            createEvent('Price', 'price.created', model, options).catch(console.error);
          },
          afterUpdate: (model: Price, options) => {
            createEvent('Price', 'price.updated', model, options).catch(console.error);
          },
          afterDestroy: (model: Price, options) => {
            createEvent('Price', 'price.deleted', model, options).catch(console.error);
          },
        },
      }
    );
  }

  public static associate(models: any) {
    this.hasOne(models.Product, {
      sourceKey: 'product_id',
      foreignKey: 'id',
      as: 'product',
    });
    this.hasOne(models.PaymentCurrency, {
      sourceKey: 'currency_id',
      foreignKey: 'id',
      as: 'currency',
    });
  }

  public static getModel(price: TPrice) {
    if (price.billing_scheme === 'tiered') {
      return price.tiers_mode;
    }

    if (price.transform_quantity) {
      return 'package';
    }

    return 'standard';
  }

  public transformQuantity(quantity: number) {
    if (this.transform_quantity) {
      if (this.transform_quantity.round === 'up') {
        return Math.ceil(quantity / this.transform_quantity.divide_by);
      }
      return Math.floor(quantity / this.transform_quantity.divide_by);
    }
    return quantity;
  }

  public static formatBeforeSave(price: Partial<TPrice & { model: string }>) {
    // Validate dynamic pricing fields
    if (price.pricing_type === 'dynamic') {
      if (!price.base_currency || !price.base_amount) {
        throw new Error('base_currency and base_amount are required for dynamic pricing');
      }
      if (price.base_currency !== 'USD') {
        throw new Error('Only USD is supported as base_currency');
      }
      // Set default lock_duration if not provided
      if (!price.dynamic_pricing_config) {
        price.dynamic_pricing_config = { lock_duration: 30 };
      }
    } else if (price.pricing_type === 'fixed') {
      // Clear dynamic pricing fields for fixed prices
      price.base_currency = null;
      price.base_amount = null;
      price.dynamic_pricing_config = null;
    }

    if (price.type) {
      if (price.type === 'recurring') {
        if (!price.recurring) {
          throw new Error('recurring config is required for recurring prices');
        }
        price.recurring.interval_count = Number(price.recurring.interval_count);
      } else {
        price.recurring = null;
      }
    }

    if (price.model) {
      if (['graduated', 'volume'].includes(price.model)) {
        price.billing_scheme = 'tiered';
        price.tiers_mode = price.model;
        if (isEmpty(price.tiers)) {
          throw new Error('tiers is required for graduated and volume prices');
        }
      } else {
        price.billing_scheme = 'per_unit';
        price.tiers = null;
      }

      if (price.model !== 'package') {
        price.transform_quantity = null;
      } else if (price.transform_quantity) {
        price.transform_quantity.divide_by = Number(price.transform_quantity.divide_by) || 1;
      }
    }

    price.metadata = formatMetadata(price.metadata);

    if (Array.isArray(price.currency_options)) {
      price.currency_options = price.currency_options.map((x) =>
        pick(x, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount'])
      );
    }

    if (price.quantity_available) {
      price.quantity_available = Number(price.quantity_available);
    }

    if (price.quantity_limit_per_checkout) {
      price.quantity_limit_per_checkout = Number(price.quantity_limit_per_checkout);
    }

    if (price.quantity_sold) {
      price.quantity_sold = Number(price.quantity_sold);
    }

    return price;
  }

  public static formatAfterRead(doc: TPrice, currencies: TPaymentCurrency[]) {
    if (doc.currency_options) {
      doc.currency_options.forEach((x) => {
        // @ts-ignore
        x.currency = currencies.find((c) => c.id === x.currency_id);
      });
    } else {
      doc.currency_options = [
        {
          currency_id: doc.currency_id,
          unit_amount: doc.unit_amount,
          // @ts-ignore
          currency: doc.currency,
          tiers: null,
          custom_unit_amount: null,
        },
      ];
    }

    return doc;
  }

  public static formatCurrencies(options: PriceCurrency[], currencies: TPaymentCurrency[]) {
    return options.map((x) => {
      const currency = currencies.find((c) => c.id === x.currency_id);
      if (!currency) {
        throw new Error(`currency ${x.currency_id} used in price not found or inactive`);
      }

      return omit(
        {
          ...x,
          unit_amount: fromTokenToUnit(x.unit_amount, currency.decimal).toString(),
          currency,
        },
        ['currency']
      ) as any;
    });
  }

  public static async expand(
    items: LineItem[],
    { product = true, upsell }: { product?: boolean; upsell?: boolean } = {}
  ): Promise<TLineItemExpanded[]> {
    const priceIds: string[] = items
      .map((i) => i.price_id)
      .concat(items.map((i) => i.upsell_price_id as string))
      .filter(Boolean);
    const prices = await Price.findAll({
      where: { id: priceIds },
      include: product ? [{ model: sequelize.models.Product, as: 'product' }] : [],
    });

    // expand currency_options
    prices.forEach((x) => {
      if (!x.currency_options || x.currency_options?.length === 0) {
        x.currency_options = [
          {
            currency_id: x.currency_id,
            unit_amount: x.unit_amount,
            tiers: null,
            custom_unit_amount: null,
          },
        ];
      }
    });

    // expand upsell
    if (upsell) {
      await Promise.all(
        prices.map(async (x) => {
          if (x.upsell?.upsells_to_id) {
            const to = await Price.findByPk(x.upsell?.upsells_to_id);
            if (to) {
              // @ts-ignore
              x.upsell.upsells_to = to;
            }
          }
          if (x.recurring?.meter_id) {
            // @ts-ignore
            x.meter = await Meter.findByPk(x.recurring?.meter_id);
          }
        })
      );
    }

    // @ts-ignore
    return items.map((x) => {
      const price = prices.find((p) => p.id === x.price_id);
      if (!price) {
        logger.warn('Price not found for line item', { priceId: x.price_id });
      }
      return {
        ...x,
        price,
        upsell_price: x.upsell_price_id ? prices.find((p) => p.id === x.upsell_price_id) : null,
      };
    }) as TLineItemExpanded[];
  }

  public static async insert(price: TPrice & { model: string }) {
    if (price.lookup_key) {
      const exist = await this.count({ where: { lookup_key: price.lookup_key } });
      if (exist) {
        throw new Error('lookup_key already exists');
      }
    }

    // @ts-ignore
    return this.create(this.formatBeforeSave({ model: 'standard', ...price }));
  }

  public static findByPkOrLookupKey(id: string, options: FindOptions<Price> = {}) {
    return this.findOne({
      where: { [Op.or]: [{ id }, { lookup_key: id }] },
      ...options,
    });
  }

  public async isUsed(checkProduct: boolean = false) {
    const { SubscriptionItem, InvoiceItem, Product } = this.sequelize.models;

    let used = checkProduct ? (await Product!.count({ where: { default_price_id: this.id } })) > 0 : false;
    if (used) {
      logger.info('Price is used as default product price', { priceId: this.id });
      return true;
    }

    // @ts-ignore
    used = await InvoiceItem!.isPriceUsed(this.id);
    if (used) {
      logger.info('Price is used by invoice item', { priceId: this.id });
      return true;
    }

    // @ts-ignore
    used = await SubscriptionItem!.isPriceUsed(this.id);
    if (used) {
      logger.info('Price is used by subscription item', { priceId: this.id });
      return true;
    }

    // @ts-ignore
    let [{ count }] = await this.sequelize.query(
      `SELECT count(*) AS count FROM pricing_tables JOIN json_each(items) AS item ON json_extract(item.value, '$.price_id') = '${this.id}'`,
      { type: QueryTypes.SELECT }
    );
    used = count > 0;
    if (used) {
      logger.info('Price is used by pricing table', { priceId: this.id });
      return true;
    }

    // @ts-ignore
    [{ count }] = await this.sequelize.query(
      `SELECT count(*) AS count FROM payment_links JOIN json_each(line_items) AS item ON json_extract(item.value, '$.price_id') = '${this.id}'`,
      { type: QueryTypes.SELECT }
    );
    used = count > 0;
    if (used) {
      logger.info('Price is used by payment link', { priceId: this.id });
      return true;
    }

    // @ts-ignore
    [{ count }] = await this.sequelize.query(
      `SELECT count(cs.id) AS count FROM checkout_sessions AS cs JOIN json_each(line_items) AS item ON json_extract(item.value, '$.price_id') = '${this.id}' WHERE cs.status != 'expired'`,
      { type: QueryTypes.SELECT }
    );
    used = count > 0;
    if (used) {
      logger.info('Price is used by active checkout session', { priceId: this.id });
      return true;
    }

    return false;
  }
}

export type TPrice = InferAttributes<Price>;
