/* eslint-disable @typescript-eslint/lines-between-class-members */
import {
  CreationOptional,
  DataTypes,
  InferAttributes,
  InferCreationAttributes,
  Model,
  Op,
  FindOptions,
  WhereOptions,
} from 'sequelize';
import { fromTokenToUnit } from '@ocap/util';

import type { LiteralUnion } from 'type-fest';
import { createIdGenerator, formatMetadata } from '../../libs/util';
import { trimDecimals } from '../../libs/math-utils';
import type { NFTConfig, Restrictions, VCConfig, VerificationType } from './types';
import type { TPaymentCurrency } from './payment-currency';
import { createEvent } from '../../libs/audit';

const nextId = createIdGenerator('promo', 24);

// @link https://stripe.com/docs/api/promotion_codes
// eslint-disable-next-line prettier/prettier
export class PromotionCode extends Model<InferAttributes<PromotionCode>, InferCreationAttributes<PromotionCode>> {
  declare id: CreationOptional<string>;
  declare livemode: boolean;
  declare active: boolean;
  declare locked: CreationOptional<boolean>;

  declare code: string;

  declare description?: string;

  declare coupon_id: string;

  // redeem conditions
  declare max_redemptions?: number;
  declare restrictions?: Restrictions;
  declare customer_id?: string;

  declare times_redeemed?: number;

  declare expires_at?: number;
  declare created_at: CreationOptional<Date>;
  declare updated_at: CreationOptional<Date>;

  // Separated verification configurations
  declare verification_type?: VerificationType;
  declare nft_config?: NFTConfig;
  declare vc_config?: VCConfig;
  declare customer_dids?: string[];
  declare metadata?: Record<string, any>;
  declare created_via: LiteralUnion<'api' | 'dashboard' | 'portal', string>;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(30),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    locked: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
    },
    active: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
    },
    code: {
      type: DataTypes.STRING(16),
      allowNull: false,
    },
    description: {
      type: DataTypes.TEXT,
      allowNull: true,
    },
    coupon_id: {
      type: DataTypes.STRING(30),
      allowNull: false,
    },
    max_redemptions: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    restrictions: {
      type: DataTypes.JSON,
      defaultValue: {},
    },
    customer_id: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    times_redeemed: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    created_via: {
      type: DataTypes.ENUM('api', 'dashboard', 'portal'),
    },
    expires_at: {
      type: DataTypes.INTEGER,
      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(
      {
        ...PromotionCode.GENESIS_ATTRIBUTES,
        verification_type: {
          type: DataTypes.ENUM('code', 'nft', 'vc', 'user_restricted'),
          defaultValue: 'code',
          allowNull: false,
        },
        nft_config: {
          type: DataTypes.JSON,
          allowNull: true,
        },
        vc_config: {
          type: DataTypes.JSON,
          allowNull: true,
        },
        customer_dids: {
          type: DataTypes.JSON,
          allowNull: true,
        },
        metadata: {
          type: DataTypes.JSON,
          allowNull: true,
        },
      },
      {
        sequelize,
        modelName: 'PromotionCode',
        tableName: 'promotion_codes',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: PromotionCode, options) => {
            createEvent('PromotionCode', 'promotion_code.created', model, options).catch(console.error);
          },
          afterUpdate: (model: PromotionCode, options) => {
            createEvent('PromotionCode', 'promotion_code.updated', model, options).catch(console.error);
          },
          afterDestroy: (model: PromotionCode, options) => {
            createEvent('PromotionCode', 'promotion_code.deleted', model, options).catch(console.error);
          },
        },
      }
    );
  }

  public static associate(models: any) {
    this.belongsTo(models.Coupon, {
      foreignKey: 'coupon_id',
      as: 'coupon',
    });
  }

  public static formatBeforeSave(promotionCode: Partial<TPromotionCode>) {
    if (promotionCode.metadata) {
      promotionCode.metadata = formatMetadata(promotionCode.metadata);
    }

    if (promotionCode.max_redemptions) {
      promotionCode.max_redemptions = Number(promotionCode.max_redemptions);
    }

    if (promotionCode.times_redeemed) {
      promotionCode.times_redeemed = Number(promotionCode.times_redeemed);
    }

    return promotionCode;
  }

  /**
   * Find promotion code by multiple criteria:
   * - id: promotion code ID
   * - code: promotion code string
   * - nft_address: NFT contract address (when verification_type is 'nft')
   * - vc_role: VC role (when verification_type is 'vc')
   * - userDid: user DID (when verification_type is 'user_restricted')
   */
  public static findByMultipleCriteria(
    searchValue: string,
    options: FindOptions<PromotionCode> = {},
    verificationType: VerificationType | null = null
  ) {
    const whereConditions = [{ id: searchValue }, { code: searchValue }] as WhereOptions[];

    // Add NFT address search condition
    if (verificationType === 'nft' || !verificationType) {
      whereConditions.push({
        verification_type: 'nft',
        'nft_config.addresses': {
          [Op.contains]: [searchValue],
        },
      });
    }

    // Add VC role search condition
    if (verificationType === 'vc' || !verificationType) {
      whereConditions.push({
        verification_type: 'vc',
        'vc_config.roles': {
          [Op.contains]: [searchValue],
        },
      });
    }

    // Add user DID search condition
    if (verificationType === 'user_restricted' || !verificationType) {
      whereConditions.push({
        verification_type: 'user_restricted',
        customer_dids: {
          [Op.contains]: [searchValue],
        },
      });
    }

    return this.findOne({
      where: { [Op.or]: whereConditions },
      ...options,
    });
  }

  /**
   * Find promotion code by specific criteria type
   */
  public static findByCode(code: string, options: FindOptions<PromotionCode> = {}) {
    return this.findOne({
      where: { code },
      ...options,
    });
  }

  public static findByNftAddress(nftAddress: string, options: FindOptions<PromotionCode> = {}) {
    return this.findOne({
      where: {
        verification_type: 'nft',
        'nft_config.addresses': {
          [Op.contains]: [nftAddress],
        },
      } as any,
      ...options,
    });
  }

  public static findByVcRole(vcRole: string, options: FindOptions<PromotionCode> = {}) {
    return this.findOne({
      where: {
        verification_type: 'vc',
        'vc_config.roles': {
          [Op.contains]: [vcRole],
        },
      } as any,
      ...options,
    });
  }

  public static findByUserDid(userDid: string, options: FindOptions<PromotionCode> = {}) {
    return this.findOne({
      where: {
        verification_type: 'user_restricted',
        customer_dids: {
          [Op.contains]: [userDid],
        },
      } as any,
      ...options,
    });
  }

  /**
   * Check if promotion code is being used
   */
  public async isUsed() {
    const { Discount } = this.sequelize.models;
    const discountCount = await Discount!.count({
      where: { promotion_code_id: this.id },
    });

    if (discountCount > 0) {
      return true;
    }
    return false;
  }

  /**
   * Check if promotion code is valid for use
   */
  public isValid() {
    if (!this.active) {
      return { valid: false, reason: 'Promotion code is inactive' };
    }

    if (this.expires_at && Math.floor(Date.now() / 1000) > this.expires_at) {
      return { valid: false, reason: 'Promotion code has expired' };
    }

    if (this.max_redemptions && (this.times_redeemed ?? 0) >= this.max_redemptions) {
      return { valid: false, reason: 'Promotion code has reached maximum redemptions' };
    }

    return { valid: true };
  }

  /**
   * Insert promotion code with formatting
   */
  public static insert(promotionCode: Partial<TPromotionCode>) {
    const formattedData = this.formatBeforeSave({
      active: true,
      times_redeemed: 0,
      metadata: {},
      ...promotionCode,
    });
    return this.create(formattedData as any);
  }

  /**
   * Format restrictions currency options for storage - convert unit amounts to token amounts
   */
  public static formatRestrictionsCurrencyOptions(
    restrictions: Restrictions | undefined,
    currencies: TPaymentCurrency[]
  ): Restrictions | undefined {
    if (!restrictions || !restrictions.currency_options) {
      return restrictions;
    }

    const formatted: Record<string, { minimum_amount: string }> = {};

    if (restrictions.minimum_amount && restrictions.minimum_amount_currency) {
      const currency = currencies.find((c) => c.id === restrictions.minimum_amount_currency);
      if (!currency) {
        throw new Error(`currency ${restrictions.minimum_amount_currency} not found or inactive`);
      }
      restrictions.minimum_amount = fromTokenToUnit(
        trimDecimals(restrictions.minimum_amount, currency.decimal),
        currency.decimal
      ).toString();
    }

    for (const [currencyId, optionData] of Object.entries(restrictions.currency_options)) {
      const currency = currencies.find((c) => c.id === currencyId);
      if (!currency) {
        throw new Error(`currency ${currencyId} used in promotion code not found or inactive`);
      }

      formatted[currencyId] = {
        minimum_amount: fromTokenToUnit(
          trimDecimals(optionData.minimum_amount || '0', currency.decimal),
          currency.decimal
        ).toString(),
      };

      if (restrictions.minimum_amount && !restrictions.currency_options[currencyId]) {
        restrictions.currency_options[currencyId] = {
          minimum_amount: restrictions.minimum_amount,
        };
      }
    }

    // Return with the correct storage format (string values)
    return {
      ...restrictions,
      currency_options: formatted,
    } as any;
  }
}

export type TPromotionCode = InferAttributes<PromotionCode>;
