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

import { createEvent } from '../../libs/audit';
import { createIdGenerator } from '../../libs/util';
import dayjs from '../../libs/dayjs';
import { CreditGrantApplicabilityConfig, CreditGrantChainDetail, CreditGrantChainStatus } from './types';
import logger from '../../libs/logger';
import { PaymentCurrency, TPaymentCurrency } from './payment-currency';
import { Meter, TMeter } from './meter';

const CREDIT_GRANT_STATUS_EVENTS = {
  depleted: 'depleted',
  expired: 'expired',
  voided: 'voided',
};

type CreditGrantSummary = {
  paymentCurrency: TPaymentCurrency;
  meter?: TMeter;
  totalAmount: string;
  remainingAmount: string;
  grantCount: number;
};

// 创建状态变化事件
async function createCreditGrantStatusEvent(model: CreditGrant, options: any): Promise<void> {
  if (model.changed('status')) {
    const previousStatus = model.previous('status');
    const currentStatus = model.status;
    if (previousStatus === 'pending' && currentStatus === 'granted') {
      await createEvent('CreditGrant', 'customer.credit_grant.granted', model, options);
      return;
    }
    const statusEvent = CREDIT_GRANT_STATUS_EVENTS[currentStatus as keyof typeof CREDIT_GRANT_STATUS_EVENTS];
    if (statusEvent) {
      await createEvent('CreditGrant', `customer.credit_grant.${statusEvent}`, model, options);
    }
  }
}

export const nextCreditGrantId = createIdGenerator('credgr', 14);

export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreationAttributes<CreditGrant>> {
  declare id: CreationOptional<string>;
  declare object: CreationOptional<string>;
  declare amount: string;
  declare currency_id: string;
  declare applicability_config?: CreditGrantApplicabilityConfig;
  declare category: LiteralUnion<'paid' | 'promotional', string>;
  declare customer_id: string;
  declare effective_at?: number;
  declare expires_at?: number;
  declare livemode: boolean;
  declare metadata?: Record<string, any>;
  declare name?: string;
  declare priority: number; // 0-100, 0为最高优先级
  declare test_clock?: string;
  declare voided_at?: number;

  // 状态相关字段
  declare status: LiteralUnion<'pending' | 'granted' | 'depleted' | 'expired' | 'voided', string>;
  declare remaining_amount: string; // 剩余金额

  // On-chain credit token operation fields
  declare chain_status?: CreditGrantChainStatus;
  declare chain_detail?: CreditGrantChainDetail;

  // 审计字段
  declare created_by?: string;
  declare updated_by?: string;
  declare created_via: LiteralUnion<'api' | 'dashboard' | 'portal', string>;

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

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(18),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextCreditGrantId,
    },
    object: {
      type: DataTypes.STRING(32),
      defaultValue: 'credit_grant',
      allowNull: false,
    },
    amount: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    currency_id: {
      type: DataTypes.STRING(15),
      allowNull: false,
    },
    applicability_config: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    category: {
      type: DataTypes.ENUM('paid', 'promotional'),
      allowNull: false,
    },
    customer_id: {
      type: DataTypes.STRING(18),
      allowNull: false,
    },
    effective_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    expires_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    name: {
      type: DataTypes.STRING(255),
      allowNull: true,
    },
    priority: {
      type: DataTypes.INTEGER,
      defaultValue: 50,
      allowNull: false,
      validate: {
        min: 0,
        max: 100,
      },
    },
    test_clock: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    voided_at: {
      type: DataTypes.INTEGER,
      allowNull: true,
    },
    status: {
      type: DataTypes.ENUM('pending', 'granted', 'depleted', 'expired', 'voided'),
      defaultValue: 'granted',
      allowNull: false,
    },
    remaining_amount: {
      type: DataTypes.STRING(32),
      allowNull: false,
    },
    chain_status: {
      type: DataTypes.ENUM(
        'mint_pending',
        'mint_completed',
        'mint_failed',
        'burn_pending',
        'burn_completed',
        'burn_failed'
      ),
      allowNull: true,
    },
    chain_detail: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    created_by: {
      type: DataTypes.STRING(40),
      allowNull: true,
    },
    updated_by: {
      type: DataTypes.STRING(40),
      allowNull: true,
    },
    created_via: {
      type: DataTypes.ENUM('api', 'dashboard', 'portal'),
      allowNull: false,
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  // check if Credit Grant is available
  public isAvailable(): boolean {
    const now = dayjs().unix();

    if (this.status !== 'granted') {
      return false;
    }

    if (this.effective_at && this.effective_at > now) {
      return false;
    }

    if (this.expires_at && this.expires_at <= now) {
      return false;
    }

    if (new BN(this.remaining_amount).lte(new BN(0))) {
      return false;
    }

    return true;
  }

  // consume credit
  public async consumeCredit(
    amount: string,
    _context: {
      subscription_id?: string;
      meter_event_id?: string;
    },
    dryRun: boolean = false
  ): Promise<{
    consumed: string;
    remaining: string;
    depleted: boolean;
  }> {
    const requestedAmount = new BN(amount);
    const availableAmount = new BN(this.remaining_amount);

    logger.debug('Consume credit in model', {
      requestedAmount: requestedAmount.toString(),
      availableAmount: availableAmount.toString(),
      grantId: this.id,
    });

    if (requestedAmount.lte(new BN(0))) {
      throw new Error('Amount must be greater than 0');
    }

    if (!this.isAvailable()) {
      throw new Error('Credit Grant is not available for consumption');
    }

    // calculate actual consumable amount
    const consumedAmount = requestedAmount.lte(availableAmount) ? requestedAmount : availableAmount;
    const newRemainingAmount = availableAmount.sub(consumedAmount);
    const isDepleted = newRemainingAmount.lte(new BN(0));

    if (!dryRun) {
      // update remaining amount
      this.remaining_amount = newRemainingAmount.toString();

      // if depleted, update status
      if (isDepleted) {
        this.status = 'depleted';
      }

      await this.save();

      // Fire-and-forget: audit event should not block the consumption hot path.
      // Trade-off: if the process crashes between save() and this call, the audit event is lost,
      // but grant state is already persisted and can be reconciled from DB.
      createEvent('CreditGrant', 'customer.credit_grant.consumed', this).catch(console.error);
    }

    return {
      consumed: consumedAmount.toString(),
      remaining: newRemainingAmount.toString(),
      depleted: isDepleted,
    };
  }

  // check if Credit Grant is applicable to specified price
  public isApplicableToPrice(priceId: string): boolean {
    if (!this.applicability_config) {
      return true; // no limit config, default to applicable to all
    }

    const { scope } = this.applicability_config;

    // if specified price list, check if in the list
    if (scope.prices && Array.isArray(scope.prices) && scope.prices.length > 0) {
      return scope.prices.includes(priceId);
    }

    return true;
  }

  // check if Credit Grant has on-chain token (mint completed)
  public hasOnchainToken(): boolean {
    return CreditGrant.hasOnchainToken(this);
  }

  // Static version that works with plain objects
  public static hasOnchainToken(grant: { chain_status?: string | null }): boolean {
    return grant.chain_status === 'mint_completed';
  }

  public static initialize(sequelize: any) {
    this.init(this.GENESIS_ATTRIBUTES, {
      sequelize,
      modelName: 'CreditGrant',
      tableName: 'credit_grants',
      createdAt: 'created_at',
      updatedAt: 'updated_at',
      indexes: [
        { fields: ['status'], name: 'idx_credit_grant_status' },
        { fields: ['effective_at'], name: 'idx_credit_grant_effective_at' },
        { fields: ['expires_at'], name: 'idx_credit_grant_expires_at' },
      ],
      hooks: {
        afterCreate: (model: CreditGrant, options) => {
          createEvent('CreditGrant', 'customer.credit_grant.created', model, options).catch(console.error);
        },
        afterUpdate: (model: CreditGrant, options) => {
          createEvent('CreditGrant', 'customer.credit_grant.updated', model, options).catch(console.error);

          if (model.changed('status')) {
            createCreditGrantStatusEvent(model, options).catch(console.error);
          }
        },
        afterDestroy: (model: CreditGrant, options) =>
          createEvent('CreditGrant', 'customer.credit_grant.deleted', model, options).catch(console.error),
      },
    });
  }

  public static associate(models: any) {
    this.belongsTo(models.Customer, {
      foreignKey: 'customer_id',
      as: 'customer',
    });

    this.hasOne(models.PaymentCurrency, {
      foreignKey: 'id',
      sourceKey: 'currency_id',
      as: 'paymentCurrency',
    });

    this.hasMany(models.CreditTransaction, {
      foreignKey: 'credit_grant_id',
      as: 'transactions',
    });
  }

  // get available Credit Grants for customer (sorted by priority)
  public static async getAvailableCreditsForCustomer(
    customerId: string,
    currencyId: string,
    priceIds?: string[] | string
  ): Promise<CreditGrant[]> {
    const now = dayjs().unix();

    const whereClause: any = {
      customer_id: customerId,
      currency_id: currencyId,
      status: 'granted',
      remaining_amount: { [Op.gt]: '0' },
      [Op.and]: [
        // effective_at check: null/0表示立即生效，或者已经生效
        {
          [Op.or]: [{ effective_at: null }, { effective_at: 0 }, { effective_at: { [Op.lte]: now } }],
        },
        // expires_at check: null/0表示永不过期，或者还未过期
        {
          [Op.or]: [{ expires_at: null }, { expires_at: 0 }, { expires_at: { [Op.gt]: now } }],
        },
      ],
    };

    const grants = await this.findAll({
      where: whereClause,
      order: [
        // 1. 先应用较早过期的 credit grants (expires_at 为 null 视为永不过期，排在后面)
        [literal('CASE WHEN expires_at IS NULL THEN 1 ELSE 0 END'), 'ASC'],
        ['expires_at', 'ASC'],
        // 2. 然后是 promotional 类型的 credit grants
        [literal("CASE WHEN category = 'promotional' THEN 0 ELSE 1 END"), 'ASC'],
        // 3. 再是较早生效的 credit grants (effective_at 为 null 视为立即生效，排在前面)
        [literal('CASE WHEN effective_at IS NULL THEN 0 ELSE 1 END'), 'ASC'],
        ['effective_at', 'ASC'],
        // 4. 最后是较早创建的 credit grants
        ['created_at', 'ASC'],
      ],
    });

    if (!grants || grants.length === 0) {
      return [];
    }

    const existPrice = Array.isArray(priceIds) ? priceIds.length > 0 : priceIds;
    // if specified price id, further filter applicable grants
    if (existPrice && priceIds) {
      const priceIdList = Array.isArray(priceIds) ? priceIds : [priceIds];
      return grants.filter((grant) => priceIdList.some((priceId) => grant.isApplicableToPrice(priceId)));
    }

    return grants;
  }

  // batch set expired
  public static async expireCredits(grantIds: string[]): Promise<number> {
    const now = dayjs().unix();

    const [affectedCount] = await this.update(
      {
        status: 'expired',
        expires_at: now,
      },
      {
        where: {
          id: { [Op.in]: grantIds },
          status: { [Op.in]: ['pending', 'granted'] },
        },
      }
    );

    return affectedCount;
  }

  /**
   * 获取客户的有效信用额度总额（按货币分组）
   */
  public static async getEffectiveCreditSummary({
    customerId,
    currencyId: searchCurrencyId,
    priceIds,
  }: {
    customerId: string;
    currencyId?: string[] | string;
    priceIds?: string[];
  }): Promise<Record<string, CreditGrantSummary>> {
    const summary: Record<string, CreditGrantSummary> = {};

    let targetCurrencyIds: string[] = [];
    if (searchCurrencyId) {
      targetCurrencyIds = typeof searchCurrencyId === 'string' ? [searchCurrencyId] : searchCurrencyId;
    }

    if (!searchCurrencyId) {
      const grantsWithCurrency = await this.findAll({
        where: {
          customer_id: customerId,
        },
        attributes: ['currency_id'],
        group: ['currency_id'],
        raw: true,
      });

      targetCurrencyIds = grantsWithCurrency.map((grant: any) => grant.currency_id);
    }

    const currencies = await PaymentCurrency.scope('withRechargeConfig').findAll({
      where: {
        id: {
          [Op.in]: Array.from(new Set(targetCurrencyIds)),
        },
      },
    });
    const currencyMap = new Map(currencies.map((c) => [c.id, c]));

    // Query meters for credit currencies
    const meters = await Meter.findAll({
      where: {
        currency_id: {
          [Op.in]: Array.from(new Set(targetCurrencyIds)),
        },
      },
    });
    const meterMap = new Map(meters.map((m) => [m.currency_id, m]));

    await Promise.all(
      targetCurrencyIds.map(async (currencyId: string) => {
        const paymentCurrency = currencyMap.get(currencyId);
        if (!paymentCurrency) {
          return null;
        }
        if (!paymentCurrency.isCredit()) {
          return null;
        }

        const meter = meterMap.get(currencyId);
        const availableGrants = await this.getAvailableCreditsForCustomer(customerId, currencyId, priceIds);

        if (availableGrants.length > 0) {
          let totalAmount = '0';
          let remainingAmount = '0';
          let grantCount = 0;

          availableGrants.forEach((grant) => {
            totalAmount = new BN(totalAmount).add(new BN(grant.amount)).toString();
            remainingAmount = new BN(remainingAmount).add(new BN(grant.remaining_amount)).toString();
            grantCount += 1;
          });

          const result: CreditGrantSummary = {
            paymentCurrency,
            meter,
            totalAmount,
            remainingAmount,
            grantCount,
          };
          summary[currencyId] = result;

          return result;
        }
        summary[currencyId] = {
          paymentCurrency,
          meter,
          totalAmount: '0',
          remainingAmount: '0',
          grantCount: 0,
        };
        return null;
      })
    );

    return summary;
  }
}

export type TCreditGrant = InferAttributes<CreditGrant>;
