import { Op } from 'sequelize';
import debounce from 'lodash/debounce';
/* eslint-disable @typescript-eslint/indent */
import { events } from '../libs/event';
import logger from '../libs/logger';
import dayjs from '../libs/dayjs';
import { Notification } from '../libs/notification';
import type { BaseEmailTemplate } from '../libs/notification/template/base';
import {
  CustomerRewardSucceededEmailTemplate,
  CustomerRewardSucceededEmailTemplateOptions,
} from '../libs/notification/template/customer-reward-succeeded';
import {
  OneTimePaymentSucceededEmailTemplate,
  OneTimePaymentSucceededEmailTemplateOptions,
} from '../libs/notification/template/one-time-payment-succeeded';
import {
  SubscriptionCanceledEmailTemplate,
  SubscriptionCanceledEmailTemplateOptions,
} from '../libs/notification/template/subscription-canceled';
import {
  SubscriptionRefundSucceededEmailTemplate,
  SubscriptionRefundSucceededEmailTemplateOptions,
} from '../libs/notification/template/subscription-refund-succeeded';
import {
  OneTimePaymentRefundSucceededEmailTemplate,
  OneTimePaymentRefundSucceededEmailTemplateOptions,
} from '../libs/notification/template/one-time-payment-refund-succeeded';
import {
  SubscriptionRenewFailedEmailTemplate,
  SubscriptionRenewFailedEmailTemplateOptions,
} from '../libs/notification/template/subscription-renew-failed';
import {
  SubscriptionRenewedEmailTemplate,
  SubscriptionRenewedEmailTemplateOptions,
} from '../libs/notification/template/subscription-renewed';
import {
  SubscriptionSucceededEmailTemplate,
  SubscriptionSucceededEmailTemplateOptions,
} from '../libs/notification/template/subscription-succeeded';
import {
  SubscriptionTrialStartEmailTemplate,
  SubscriptionTrialStartEmailTemplateOptions,
} from '../libs/notification/template/subscription-trial-start';
import {
  SubscriptionTrialWillEndEmailTemplate,
  SubscriptionTrialWillEndEmailTemplateOptions,
} from '../libs/notification/template/subscription-trial-will-end';
import {
  SubscriptionUpgradedEmailTemplate,
  SubscriptionUpgradedEmailTemplateOptions,
} from '../libs/notification/template/subscription-upgraded';
import {
  SubscriptionWillCanceledEmailTemplate,
  SubscriptionWillCanceledEmailTemplateOptions,
} from '../libs/notification/template/subscription-will-canceled';
import {
  SubscriptionWillRenewEmailTemplate,
  SubscriptionWillRenewEmailTemplateOptions,
} from '../libs/notification/template/subscription-will-renew';
import {
  SubscriptionStakeSlashSucceededEmailTemplate,
  SubscriptionStakeSlashSucceededEmailTemplateOptions,
} from '../libs/notification/template/subscription-stake-slash-succeeded';
import createQueue from '../libs/queue';
import {
  CheckoutSession,
  EventType,
  Invoice,
  PaymentLink,
  Payout,
  Refund,
  Subscription,
  Customer,
  CreditGrant,
} from '../store/models';
import {
  UsageReportEmptyEmailTemplate,
  UsageReportEmptyEmailTemplateOptions,
} from '../libs/notification/template/usage-report-empty';
import {
  BillingDiscrepancyEmailTemplate,
  BillingDiscrepancyEmailTemplateOptions,
} from '../libs/notification/template/billing-discrepancy';
import {
  OverdraftProtectionExhaustedEmailTemplate,
  OverdraftProtectionExhaustedEmailTemplateOptions,
} from '../libs/notification/template/subscription-overdraft-protection-exhausted';
import {
  CustomerCreditInsufficientEmailTemplate,
  CustomerCreditInsufficientEmailTemplateOptions,
} from '../libs/notification/template/customer-credit-insufficient';
import {
  CustomerCreditGrantGrantedEmailTemplate,
  CustomerCreditGrantGrantedEmailTemplateOptions,
} from '../libs/notification/template/customer-credit-grant-granted';

import {
  CustomerCreditLowBalanceEmailTemplate,
  CustomerCreditLowBalanceEmailTemplateOptions,
} from '../libs/notification/template/customer-credit-low-balance';
import {
  CustomerAutoRechargeFailedEmailTemplate,
  CustomerAutoRechargeFailedEmailTemplateOptions,
} from '../libs/notification/template/customer-auto-recharge-failed';
import {
  CustomerAutoRechargeDailyLimitExceededEmailTemplate,
  CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions,
} from '../libs/notification/template/customer-auto-recharge-daily-limit-exceeded';
import {
  CustomerRevenueSucceededEmailTemplate,
  CustomerRevenueSucceededEmailTemplateOptions,
} from '../libs/notification/template/customer-revenue-succeeded';
import {
  AggregatedSubscriptionRenewedEmailTemplate,
  AggregatedSubscriptionRenewedEmailTemplateOptions,
} from '../libs/notification/template/aggregated-subscription-renewed';
import {
  WebhookEndpointFailedEmailTemplate,
  WebhookEndpointFailedEmailTemplateOptions,
} from '../libs/notification/template/webhook-endpoint-failed';
import {
  SubscriptionSlippageWarningEmailTemplate,
  SubscriptionSlippageWarningEmailTemplateOptions,
} from '../libs/notification/template/subscription-slippage-warning';
import {
  SubscriptionSlippageExceededEmailTemplate,
  SubscriptionSlippageExceededEmailTemplateOptions,
} from '../libs/notification/template/subscription-slippage-exceeded';
import {
  ExchangeRateAlertEmailTemplate,
  ExchangeRateAlertEmailTemplateOptions,
} from '../libs/notification/template/exchange-rate-alert';
import type { TJob } from '../store/models/job';

export type NotificationQueueJobOptions = any;

interface NotificationPreference {
  frequency: 'default' | 'daily' | 'weekly' | 'monthly';
  schedule?: {
    time: string; // HH:mm 格式
    date?: number; // weekly: 0-6, monthly: 1-31
  };
}

export type NotificationQueueJobType =
  | EventType
  | 'customer.subscription.will_renew'
  | 'customer.subscription.trial_will_end'
  | 'customer.subscription.will_canceled'
  | 'customer.reward.succeeded'
  | 'usage.report.empty'
  | 'billing.discrepancy'
  | 'subscription.overdraftProtection.exhausted'
  | 'customer.credit.insufficient'
  | 'customer.credit_grant.granted'
  | 'customer.credit.low_balance'
  | 'customer.auto_recharge.failed'
  | 'customer.auto_recharge.daily_limit_exceeded'
  | 'webhook.endpoint.failed'
  | 'exchange_rate.providers_unavailable'
  | 'exchange_rate.spread_exceeded'
  | 'subscription.slippage_warning'
  | 'subscription.slippage_exceeded';

export type NotificationQueueJob = {
  type: NotificationQueueJobType;
  options: NotificationQueueJobOptions;
};

export interface AggregatedNotificationJob {
  type: NotificationQueueJobType;
  options: {
    customer_id: string;
    items: NotificationItem[];
    time_range: {
      start: number;
      end: number;
    };
  };
}

function calculateNextNotificationTime(preference: NotificationPreference): number {
  if (preference.frequency === 'default' || !preference.schedule) {
    return Math.floor(Date.now() / 1000);
  }

  const now = dayjs();
  const [hour = 10, minute = 0] = preference.schedule?.time ? preference.schedule.time.split(':').map(Number) : [10, 0];
  let nextTime = dayjs().hour(hour).minute(minute).second(0);
  const targetDate = preference.schedule?.date ?? 1;

  switch (preference.frequency) {
    case 'daily':
      if (nextTime.isBefore(now)) {
        nextTime = nextTime.add(1, 'day');
      }
      break;

    case 'weekly':
      nextTime = nextTime.day(targetDate);
      if (nextTime.isBefore(now)) {
        nextTime = nextTime.add(1, 'week');
      }
      break;

    case 'monthly':
      nextTime = nextTime.date(targetDate);
      // if the date is not the same, set to the end of the month
      if (nextTime.date() !== targetDate) {
        nextTime = nextTime.endOf('month').hour(hour).minute(minute);
      }
      if (nextTime.isBefore(now)) {
        nextTime = nextTime.add(1, 'month');
        if (nextTime.date() !== targetDate) {
          nextTime = nextTime.endOf('month').hour(hour).minute(minute);
        }
      }
      break;
    default:
      throw new Error(`Unknown frequency: ${preference.frequency}`);
  }

  return Math.floor(nextTime.valueOf() / 1000);
}

async function getNotificationTemplate(job: NotificationQueueJob): Promise<BaseEmailTemplate> {
  if (job.type === 'usage.report.empty') {
    return new UsageReportEmptyEmailTemplate(job.options as UsageReportEmptyEmailTemplateOptions);
  }
  if (job.type === 'billing.discrepancy') {
    return new BillingDiscrepancyEmailTemplate(job.options as BillingDiscrepancyEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.started') {
    return new SubscriptionSucceededEmailTemplate(job.options as SubscriptionSucceededEmailTemplateOptions);
  }
  if (job.type === 'checkout.session.completed') {
    return new OneTimePaymentSucceededEmailTemplate(job.options as OneTimePaymentSucceededEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.upgraded') {
    return new SubscriptionUpgradedEmailTemplate(job.options as SubscriptionUpgradedEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.renewed') {
    return new SubscriptionRenewedEmailTemplate(job.options as SubscriptionRenewedEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.renew_failed') {
    return new SubscriptionRenewFailedEmailTemplate(job.options as SubscriptionRenewFailedEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.trial_start') {
    return new SubscriptionTrialStartEmailTemplate(job.options as SubscriptionTrialStartEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.will_renew') {
    return new SubscriptionWillRenewEmailTemplate(job.options as SubscriptionWillRenewEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.trial_will_end') {
    return new SubscriptionTrialWillEndEmailTemplate(job.options as SubscriptionTrialWillEndEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.will_canceled') {
    return new SubscriptionWillCanceledEmailTemplate(job.options as SubscriptionWillCanceledEmailTemplateOptions);
  }
  if (job.type === 'customer.subscription.deleted') {
    return new SubscriptionCanceledEmailTemplate(job.options as SubscriptionCanceledEmailTemplateOptions);
  }
  if (job.type === 'refund.succeeded') {
    const { refundId } = job.options as { refundId: string };
    const refund = await Refund.findByPk(refundId);
    if (refund?.subscription_id) {
      return new SubscriptionRefundSucceededEmailTemplate(
        job.options as SubscriptionRefundSucceededEmailTemplateOptions
      );
    }
    return new OneTimePaymentRefundSucceededEmailTemplate(
      job.options as OneTimePaymentRefundSucceededEmailTemplateOptions
    );
  }
  if (job.type === 'customer.reward.succeeded') {
    return new CustomerRewardSucceededEmailTemplate(job.options as CustomerRewardSucceededEmailTemplateOptions);
  }
  if (job.type === 'subscription.stake.slash.succeeded') {
    return new SubscriptionStakeSlashSucceededEmailTemplate(
      job.options as SubscriptionStakeSlashSucceededEmailTemplateOptions
    );
  }
  if (job.type === 'subscription.overdraftProtection.exhausted') {
    return new OverdraftProtectionExhaustedEmailTemplate(
      job.options as OverdraftProtectionExhaustedEmailTemplateOptions
    );
  }

  if (job.type === 'payout.paid') {
    return new CustomerRevenueSucceededEmailTemplate(job.options as CustomerRevenueSucceededEmailTemplateOptions);
  }

  if (job.type === 'customer.credit.insufficient') {
    return new CustomerCreditInsufficientEmailTemplate(job.options as CustomerCreditInsufficientEmailTemplateOptions);
  }

  if (job.type === 'customer.credit_grant.granted') {
    return new CustomerCreditGrantGrantedEmailTemplate(job.options as CustomerCreditGrantGrantedEmailTemplateOptions);
  }

  if (job.type === 'customer.credit.low_balance') {
    return new CustomerCreditLowBalanceEmailTemplate(job.options as CustomerCreditLowBalanceEmailTemplateOptions);
  }

  if (job.type === 'customer.auto_recharge.failed') {
    return new CustomerAutoRechargeFailedEmailTemplate(job.options as CustomerAutoRechargeFailedEmailTemplateOptions);
  }

  if (job.type === 'customer.auto_recharge.daily_limit_exceeded') {
    return new CustomerAutoRechargeDailyLimitExceededEmailTemplate(
      job.options as CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions
    );
  }

  if (job.type === 'webhook.endpoint.failed') {
    return new WebhookEndpointFailedEmailTemplate(job.options as WebhookEndpointFailedEmailTemplateOptions);
  }

  if (job.type === 'subscription.slippage_warning') {
    return new SubscriptionSlippageWarningEmailTemplate(job.options as SubscriptionSlippageWarningEmailTemplateOptions);
  }

  if (job.type === 'subscription.slippage_exceeded') {
    return new SubscriptionSlippageExceededEmailTemplate(
      job.options as SubscriptionSlippageExceededEmailTemplateOptions
    );
  }

  // Exchange rate alerts (admin notifications)
  if (job.type === 'exchange_rate.providers_unavailable' || job.type === 'exchange_rate.spread_exceeded') {
    return new ExchangeRateAlertEmailTemplate(job.options as ExchangeRateAlertEmailTemplateOptions);
  }

  throw new Error(`Unknown job type: ${job.type}`);
}

async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
  try {
    const template = await getNotificationTemplate(job);

    // Check if template class has a preflightCheck
    const TemplateClass = template.constructor as any;
    if (TemplateClass.preflightCheck) {
      const shouldSend = await TemplateClass.preflightCheck(job.options);
      if (!shouldSend) {
        logger.info('handleNotificationJob.skipped: preflight check returned false', {
          type: job.type,
        });
        return;
      }
    }

    await new Notification(template, job.type).send();
    logger.info('handleImmediateNotificationJob.success', { job });
  } catch (error) {
    logger.error('handleImmediateNotificationJob.error', error);
    throw error;
  }
}

const MAX_AGGREGATED_ITEMS = 1000;

interface NotificationItem<T = Record<string, any>> {
  event_id: string;
  occurred_at: number;
  data: T;
}

// 内存缓存，记录最近发送的通知
const notificationCache = new Map<string, number>();
// 去抖函数（按 key 去重）
const debounceHandlers = new Map<string, ReturnType<typeof debounce>>();

// 清理过期缓存的定时器
setInterval(
  () => {
    const now = Date.now();
    for (const [key, timestamp] of notificationCache.entries()) {
      // 清理超过24小时的缓存记录
      if (now - timestamp > 24 * 60 * 60 * 1000) {
        notificationCache.delete(key);
      }
    }
  },
  60 * 60 * 1000
); // 每小时清理一次

/**
 * Clear notification cache entries that match given prefix
 * Used to reset rate limiting when conditions change (e.g., credit recharged)
 */
function clearNotificationCache(prefix: string) {
  for (const [key] of notificationCache.entries()) {
    if (key.startsWith(prefix)) {
      notificationCache.delete(key);
      logger.info('Notification cache cleared', { key });
    }
  }
}

/**
 * Handles immediate notifications by pushing them directly to the notification queue
 */
function handleImmediateNotification(type: string, data: Record<string, any>, extraIds: string[] = []) {
  return addNotificationJob(type as NotificationQueueJobType, data, extraIds);
}

/**
 * Handles aggregated notifications with support for:
 * - Automatic fallback to immediate notification on error
 * - Item limit enforcement
 * - Time range tracking
 */
async function handleAggregatedNotification<T extends Record<string, any>>({
  type,
  customerId,
  preference,
  newItem,
  extraIds,
}: {
  type: NotificationQueueJobType;
  customerId: string;
  preference: NotificationPreference;
  newItem: NotificationItem<T>;
  extraIds: string[];
}) {
  try {
    const nextNotificationTime = calculateNextNotificationTime(preference);
    const jobId = `${type}.${customerId}.${nextNotificationTime}`;
    const existingJob = await aggregatedNotificationQueue.get(jobId);

    if (existingJob) {
      const updatedItems = [...(existingJob.options.items || []), newItem];

      if (updatedItems.length >= MAX_AGGREGATED_ITEMS) {
        await aggregatedNotificationQueue.push({
          id: `${jobId}.immediate`,
          job: {
            type,
            options: {
              customer_id: customerId,
              items: existingJob.options.items,
              time_range: {
                start: existingJob.options.time_range.start,
                end: Math.floor(Date.now() / 1000),
              },
            },
          },
        });
        logger.info('immediate aggregated notification job', { jobId });

        await aggregatedNotificationQueue.update(jobId, {
          job: {
            type,
            options: {
              customer_id: customerId,
              items: [newItem],
              time_range: {
                start: Math.floor(Date.now() / 1000),
                end: nextNotificationTime,
              },
            },
          },
          runAt: nextNotificationTime,
        });
        logger.info('update aggregated notification job', { jobId });
      } else {
        await aggregatedNotificationQueue.update(jobId, {
          job: {
            type,
            options: {
              customer_id: customerId,
              items: updatedItems,
              time_range: {
                start: existingJob.options.time_range.start,
                end: nextNotificationTime,
              },
            },
          },
          runAt: nextNotificationTime,
        });
      }
      logger.info('updated aggregated notification job', { jobId });
    } else {
      // Create new aggregation task
      await aggregatedNotificationQueue.push({
        id: jobId,
        job: {
          type,
          options: {
            customer_id: customerId,
            items: [newItem],
            time_range: {
              start: Math.floor(Date.now() / 1000),
              end: nextNotificationTime,
            },
          },
        },
        runAt: nextNotificationTime,
      });
    }
    logger.info('created aggregated notification job', { jobId });
  } catch (error) {
    logger.error('Failed to handle aggregated notification', { error });
    await handleImmediateNotification(type, newItem.data, extraIds);
  }
}

function getAggregatedNotificationTemplate(job: AggregatedNotificationJob): BaseEmailTemplate {
  if (job.type === 'customer.subscription.renewed') {
    return new AggregatedSubscriptionRenewedEmailTemplate(
      job.options as AggregatedSubscriptionRenewedEmailTemplateOptions
    );
  }
  throw new Error(`Unknown aggregated notification type: ${job.type}`);
}

async function handleAggregatedNotificationJob(job: AggregatedNotificationJob): Promise<void> {
  try {
    const template = await getAggregatedNotificationTemplate(job);
    await new Notification(template, job.type).send();
    logger.info('handleAggregatedNotificationJob.success', { job });
  } catch (error) {
    logger.error('handleAggregatedNotificationJob.error', error);
    throw error;
  }
}

export const notificationQueue = createQueue<NotificationQueueJob>({
  name: 'notification',
  onJob: handleNotificationJob,
  options: {
    concurrency: 10,
    maxRetries: 3,
    enableScheduledJob: true,
  },
});

export const aggregatedNotificationQueue = createQueue<AggregatedNotificationJob>({
  name: 'aggregated_notification',
  onJob: handleAggregatedNotificationJob,
  options: {
    concurrency: 10,
    maxRetries: 3,
    enableScheduledJob: true,
  },
});

/**
 *
 * @see https://team.arcblock.io/comment/discussions/0fba06ff-75b4-47d2-8c5c-f0379540ce03
 * @description
 * @export
 */
// eslint-disable-next-line require-await
export async function startNotificationQueue() {
  // 试用期开始
  events.on('customer.subscription.trial_start', (subscription: Subscription) => {
    addNotificationJob('customer.subscription.trial_start', { subscriptionId: subscription.id }, [subscription.id]);
  });

  events.on('customer.subscription.started', (subscription: Subscription) => {
    if (!subscription.trial_start) {
      // 没有试用期的 subscription 通知
      addNotificationJob('customer.subscription.started', { subscriptionId: subscription.id }, [subscription.id]);
    }
  });

  // 一次性购买成功
  events.on('checkout.session.completed', async (checkoutSession: CheckoutSession) => {
    if (checkoutSession.mode === 'payment') {
      const paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
      if (paymentLink?.submit_type === 'donate') {
        addNotificationJob('customer.reward.succeeded', { checkoutSessionId: checkoutSession.id }, [
          checkoutSession.id,
        ]);
        return;
      }

      addNotificationJob('checkout.session.completed', { checkoutSessionId: checkoutSession.id }, [checkoutSession.id]);
    }
  });

  events.on('customer.subscription.renewed', async (subscription: Subscription) => {
    const customer = await Customer.findByPk(subscription.customer_id);
    const preference = customer?.preference?.notification;
    if (!subscription.latest_invoice_id) {
      logger.warn('Missing latest_invoice_id for subscription', { subscriptionId: subscription.id });
      return;
    }

    if (!preference || preference.frequency === 'default') {
      handleImmediateNotification(
        'customer.subscription.renewed',
        {
          subscriptionId: subscription.id,
          invoiceId: subscription.latest_invoice_id,
        },
        [subscription.id, subscription.latest_invoice_id]
      );
      return;
    }

    await handleAggregatedNotification({
      type: 'customer.subscription.renewed',
      customerId: customer.id,
      preference,
      newItem: {
        event_id: `${subscription.id}.${subscription.latest_invoice_id}`,
        occurred_at: Math.floor(Date.now() / 1000),
        data: {
          subscriptionId: subscription.id,
          invoiceId: subscription.latest_invoice_id,
        },
      },
      extraIds: [subscription.id, subscription.latest_invoice_id],
    });
  });

  events.on('customer.subscription.renew_failed', async (subscription: Subscription, { invoiceId }) => {
    const invoice = await Invoice.findByPk(invoiceId || subscription.latest_invoice_id);

    logger.info('events.on', 'customer.subscription.renew_failed', {
      subscriptionId: subscription.id,
      invoiceId: subscription.latest_invoice_id,
    });

    if (invoice && subscription.metadata.renew_failed_reason) {
      addNotificationJob(
        'customer.subscription.renew_failed',
        { invoice, result: subscription.metadata.renew_failed_reason },
        [subscription.id, invoice.id]
      );
    }
  });

  events.on('customer.subscription.upgraded', (subscription: Subscription) => {
    const extraIds = [subscription.id];
    if (subscription.latest_invoice_id) {
      extraIds.push(subscription.latest_invoice_id);
    }
    addNotificationJob('customer.subscription.upgraded', { subscriptionId: subscription.id }, extraIds);
  });

  events.on('refund.succeeded', (refund: Refund) => {
    const extraIds = [refund.id];
    if (refund.subscription_id) {
      extraIds.unshift(refund.subscription_id);
    }
    addNotificationJob('refund.succeeded', { refundId: refund.id }, extraIds);
  });

  events.on('customer.subscription.deleted', (subscription: Subscription) => {
    addNotificationJob('customer.subscription.deleted', { subscriptionId: subscription.id }, [subscription.id]);
  });

  events.on('usage.report.empty', (subscription: Subscription, { usageReportStart, usageReportEnd }) => {
    addNotificationJob('usage.report.empty', { subscriptionId: subscription.id, usageReportStart, usageReportEnd }, [
      subscription.id,
    ]);
  });

  events.on('billing.discrepancy', (invoice: Invoice) => {
    addNotificationJob('billing.discrepancy', { invoiceId: invoice.id }, [invoice.id]);
  });

  events.on('subscription.overdraft_protection.exhausted', (subscription: Subscription) => {
    addNotificationJob('subscription.overdraftProtection.exhausted', { subscriptionId: subscription.id }, [
      subscription.id,
    ]);
  });

  events.on('payout.paid', (payout: Payout) => {
    if (payout.customer_id) {
      addNotificationJob('payout.paid', { payoutId: payout.id }, [payout.id]);
    }
  });

  events.on('customer.credit_grant.granted', (creditGrant: CreditGrant) => {
    // Only send notification for non-recurring grants or the first grant of recurring schedule
    const isScheduleGrant = creditGrant.metadata?.delivery_mode === 'schedule';
    const isFirstScheduleGrant = isScheduleGrant && creditGrant.metadata?.schedule_seq === 1;

    if (!isScheduleGrant || isFirstScheduleGrant) {
      addNotificationJob(
        'customer.credit_grant.granted',
        {
          creditGrantId: creditGrant.id,
        },
        [creditGrant.id]
      );
    }
  });

  events.on('customer.credit.low_balance', (customer: Customer, { metadata }: { metadata: any }) => {
    addNotificationJobWithDelay(
      'customer.credit.low_balance',
      {
        customerId: customer.id,
        currencyId: metadata.currency_id,
        availableAmount: metadata.available_amount,
        totalAmount: metadata.total_amount,
        percentage: metadata.percentage,
      },
      [customer.id, metadata.currency_id],
      true,
      24 * 3600
    );
  });

  events.on('customer.credit.insufficient', (customer: Customer, { metadata }: { metadata: any }) => {
    addNotificationJob(
      'customer.credit.insufficient',
      {
        customerId: customer.id,
        currencyId: metadata.currency_id,
        meterName: metadata.meter_name || 'Service',
        meterEventName: metadata.meter_event_name,
        requiredAmount: metadata.required_amount,
        availableAmount: metadata.available_amount,
        pendingAmount: metadata.pending_amount || '0',
        subscriptionId: metadata.subscription_id,
      },
      [customer.id, metadata.currency_id, metadata.subscription_id],
      true,
      24 * 3600 // 1天
    );
  });

  events.on(
    'customer.auto_recharge.failed',
    async (customer: Customer, { autoRechargeConfigId, invoiceId, paymentIntentId }) => {
      const invoice = await Invoice.findByPk(invoiceId);
      logger.info('addNotificationJob:customer.auto_recharge.failed', autoRechargeConfigId);
      if (invoice && autoRechargeConfigId && invoice.metadata?.auto_recharge_failed_reason) {
        addNotificationJob(
          'customer.auto_recharge.failed',
          {
            customerId: customer.id,
            autoRechargeConfigId,
            invoiceId,
            paymentIntentId,
            result: invoice.metadata?.auto_recharge_failed_reason,
          },
          [customer.id, autoRechargeConfigId, invoiceId]
        );
      }
    }
  );

  events.on(
    'customer.auto_recharge.daily_limit_exceeded',
    (customer: Customer, { autoRechargeConfigId, currencyId, currentBalance }) => {
      logger.info('addNotificationJob:customer.auto_recharge.daily_limit_exceeded', {
        customerId: customer.id,
        autoRechargeConfigId,
        currencyId,
      });
      addNotificationJob(
        'customer.auto_recharge.daily_limit_exceeded',
        {
          customerId: customer.id,
          autoRechargeConfigId,
          currencyId,
          currentBalance,
        },
        [customer.id, autoRechargeConfigId, currencyId],
        true, // prevent duplicate
        24 * 3600 // 24 hours
      );
    }
  );

  // System events - Exchange rate alerts (admin notifications)
  events.on('exchange_rate.providers_unavailable', (alertData: Record<string, any>) => {
    logger.info('Exchange rate providers unavailable event received', { alertData });
    addNotificationJob(
      'exchange_rate.providers_unavailable',
      alertData,
      ['exchange_rate.providers_unavailable', alertData.symbol],
      true, // Prevent duplicate
      30 * 60 // 30 minutes window
    );
  });

  events.on('exchange_rate.spread_exceeded', (alertData: Record<string, any>) => {
    logger.info('Exchange rate spread exceeded event received', { alertData });
    addNotificationJob(
      'exchange_rate.spread_exceeded',
      alertData,
      ['exchange_rate.spread_exceeded', alertData.symbol],
      true, // Prevent duplicate
      30 * 60 // 30 minutes window (matches service rate limit)
    );
  });

  // Auto-recharge skipped due to dynamic pricing constraints
  events.on(
    'auto_recharge.skipped',
    async (data: {
      config_id: string;
      customer_id: string;
      reason: 'slippage_exceeded' | 'exchange_rate_not_supported' | 'exchange_rate_fetch_failed';
      current_rate?: string;
      min_acceptable_rate?: string;
      payment_currency_symbol?: string;
      payment_currency_name?: string;
      credit_currency_name?: string;
    }) => {
      logger.info('Auto recharge skipped event received', {
        configId: data.config_id,
        customerId: data.customer_id,
        reason: data.reason,
        currentRate: data.current_rate,
        minAcceptableRate: data.min_acceptable_rate,
        paymentCurrencySymbol: data.payment_currency_symbol,
      });

      // Reuse customer.auto_recharge.failed notification with skipped reason
      // This ensures users are notified when auto-recharge cannot proceed
      const customer = await Customer.findByPk(data.customer_id);
      if (customer) {
        addNotificationJob(
          'customer.auto_recharge.failed',
          {
            customerId: data.customer_id,
            autoRechargeConfigId: data.config_id,
            result: {
              sufficient: false,
              reason: data.reason,
              currentRate: data.current_rate,
              minAcceptableRate: data.min_acceptable_rate,
              paymentCurrencySymbol: data.payment_currency_symbol,
              paymentCurrencyName: data.payment_currency_name,
              creditCurrencyName: data.credit_currency_name,
            },
          },
          [data.customer_id, data.config_id, data.reason],
          true, // Prevent duplicate notifications
          6 * 3600 // 6 hours window - avoid spamming if rate stays bad
        );
      }
    }
  );

  // Slippage exceeded notification
  events.on(
    'subscription.slippage_exceeded',
    (
      subscription: Subscription,
      {
        invoiceId,
        paymentIntentId,
        currentRate,
        minAcceptableRate,
      }: {
        invoiceId: string;
        paymentIntentId: string;
        currentRate: string;
        minAcceptableRate: string;
      }
    ) => {
      addNotificationJob(
        'subscription.slippage_exceeded',
        {
          subscriptionId: subscription.id,
          invoiceId,
          paymentIntentId,
          currentRate,
          minAcceptableRate,
        },
        [subscription.id, invoiceId],
        true, // Prevent duplicate
        24 * 3600 // 24 hours window
      );
    }
  );

  // Slippage warning notification (1 hour before renewal)
  events.on(
    'subscription.slippage_warning',
    (
      subscription: Subscription,
      {
        currentRate,
        minAcceptableRate,
        renewalTime,
      }: {
        currentRate: string;
        minAcceptableRate: string;
        renewalTime: number;
      }
    ) => {
      addNotificationJob(
        'subscription.slippage_warning',
        {
          subscriptionId: subscription.id,
          currentRate,
          minAcceptableRate,
          renewalTime,
        },
        [subscription.id],
        true, // Prevent duplicate
        3600 // 1 hour window - avoid duplicate warnings
      );
    }
  );

  // Clear credit notification cache when customer recharges
  // This allows customer to receive insufficient/low_balance notifications again
  events.on('customer.credit_grant.granted', (creditGrant: CreditGrant) => {
    const cacheKeyBase = `${creditGrant.customer_id}.${creditGrant.currency_id}`;
    const debounceKey = `credit_grant_clear.${cacheKeyBase}`;
    const delayMs = 5 * 60 * 1000;

    let handler = debounceHandlers.get(debounceKey);
    if (!handler) {
      handler = debounce(() => {
        clearNotificationCache(`customer.credit.insufficient.${cacheKeyBase}`);
        clearNotificationCache(`customer.credit.low_balance.${cacheKeyBase}`);
        debounceHandlers.delete(debounceKey);
        logger.info('Notification cache cleared after delay', {
          customerId: creditGrant.customer_id,
          currencyId: creditGrant.currency_id,
          delayMs,
        });
      }, delayMs);
      debounceHandlers.set(debounceKey, handler);
    }

    handler();
  });
}

export async function handleNotificationPreferenceChange(
  customerId: string,
  newPreference?: NotificationPreference
): Promise<void> {
  // 1. get pending scheduled jobs for this customer
  const pendingJobs = await aggregatedNotificationQueue.store.findJobs({
    delay: { [Op.not]: -1 },
    will_run_at: { [Op.gt]: Date.now() },
  });

  const customerJobs = pendingJobs.filter((job: TJob) => job.job?.options?.customer_id === customerId);

  if (!customerJobs.length) {
    return;
  }

  // 2. if changed to immediate notification, delete the old job and create a new one immediately
  if (newPreference?.frequency === 'default') {
    /* eslint-disable no-await-in-loop */
    for (const job of customerJobs) {
      try {
        await aggregatedNotificationQueue.delete(job.id);
        aggregatedNotificationQueue.push({
          id: job.id,
          job: {
            ...job.job,
            options: {
              ...job.job.options,
              time_range: {
                start: job.job.options.time_range.start,
                end: Math.floor(Date.now() / 1000),
              },
            },
          },
        });
      } catch (error) {
        logger.error('Failed to process immediate notification job', { error, jobId: job.id });
      }
    }
    /* eslint-enable no-await-in-loop */
    return;
  }

  // 3. for other frequency changes, recalculate the send time
  if (newPreference) {
    /* eslint-disable no-await-in-loop */
    for (const job of customerJobs) {
      try {
        const nextTime = calculateNextNotificationTime(newPreference);
        const oldScheduleTime = job.will_run_at ? job.will_run_at / 1000 : 0;

        // if next notification time is different from the old schedule time, update the job
        if (nextTime !== oldScheduleTime) {
          // Keep the original job id format but update the timestamp
          const jobIdParts = job.id.split('.');
          const jobId = `${jobIdParts.slice(0, -1).join('.')}.${nextTime}`;

          await aggregatedNotificationQueue.delete(job.id);
          aggregatedNotificationQueue.push({
            id: jobId,
            job: {
              ...job.job,
              options: {
                ...job.job.options,
                time_range: {
                  start: job.job.options.time_range.start,
                  end: nextTime,
                },
              },
            },
            runAt: nextTime,
          });
        }
      } catch (error) {
        logger.error('Failed to process notification job', { error, jobId: job.id });
      }
    }
    /* eslint-enable no-await-in-loop */
  }
}

/**
 * Add a notification job to the queue
 * @param type Notification type
 * @param options Notification options
 * @param extraIds Extra IDs for building a unique job ID, if not provided, use timestamp
 * @param preventDuplicate Whether to prevent duplicate, if true, will check the cache
 * @param duplicateWindow Duplicate prevention time window (seconds), default 600 seconds (10 minutes)
 */
export function addNotificationJob(
  type: NotificationQueueJobType,
  options: NotificationQueueJobOptions,
  extraIds: string[] = [],
  preventDuplicate: boolean = false,
  duplicateWindow: number = 600 // 10分钟
) {
  const idParts = [type];

  if (extraIds.length) {
    idParts.push(...extraIds);
  } else {
    idParts.push(Date.now().toString());
  }

  // if prevent duplicate, check the cache
  if (preventDuplicate) {
    const cacheKey = `${type}.${extraIds.join('.')}`;
    const lastSent = notificationCache.get(cacheKey);
    const now = Date.now();

    if (lastSent && now - lastSent < duplicateWindow * 1000) {
      logger.info('Notification skipped due to duplicate prevention', {
        type,
        cacheKey,
        lastSent: new Date(lastSent),
        duplicateWindow,
      });
      return Promise.resolve(); // 跳过重复通知
    }

    // 记录本次发送时间
    notificationCache.set(cacheKey, now);
  }

  return notificationQueue.push({
    id: idParts.join('.'),
    job: {
      type,
      options,
    },
  });
}

/**
 * Add a notification job with delay support
 * Use this when the template class has a static `delay` property
 */
export async function addNotificationJobWithDelay(
  type: NotificationQueueJobType,
  options: NotificationQueueJobOptions,
  extraIds: string[] = [],
  preventDuplicate: boolean = false,
  duplicateWindow: number = 600
) {
  const idParts = [type];

  if (extraIds.length) {
    idParts.push(...extraIds);
  } else {
    idParts.push(Date.now().toString());
  }

  if (preventDuplicate) {
    const cacheKey = `${type}.${extraIds.join('.')}`;
    const lastSent = notificationCache.get(cacheKey);
    const now = Date.now();

    if (lastSent && now - lastSent < duplicateWindow * 1000) {
      logger.info('Notification skipped due to duplicate prevention', {
        type,
        cacheKey,
        lastSent: new Date(lastSent),
        duplicateWindow,
      });
      return Promise.resolve();
    }

    notificationCache.set(cacheKey, now);
  }

  // Get delay from template class
  const template = await getNotificationTemplate({ type, options });
  const TemplateClass = template.constructor as any;
  const { delay } = TemplateClass;

  return notificationQueue.push({
    id: idParts.join('.'),
    job: {
      type,
      options,
    },
    ...(delay && delay > 0 ? { delay } : {}),
  });
}
