import { Notification } from '@blocklet/sdk';
import { getUrl } from '@blocklet/sdk/lib/component';
import { env } from '@blocklet/sdk/lib/config';
import { fromUnitToToken, BN } from '@ocap/util';
import { Op } from 'sequelize';
import { withQuery } from 'ufo';
import { getUserLocale } from '../integrations/blocklet/notification';
import dayjs from '../libs/dayjs';
import { overdueThreshold } from '../libs/env';
import logger from '../libs/logger';
import type { BaseEmailTemplate, BaseEmailTemplateType } from '../libs/notification/template/base';
import { getOwnerDid } from '../libs/util';
import { translate } from '../locales';
import {
  Event,
  Invoice,
  InvoiceItem,
  Meter,
  MeterEvent,
  PaymentCurrency,
  PaymentIntent,
  Price,
  Refund,
  Subscription,
} from '../store/models';

function getAppName() {
  return env.appName;
}

interface PendingConsumptionMetrics {
  customerCount: number;
  pendingAmounts: string;
  exceedsThreshold: boolean;
}

interface FinancialMetrics {
  totalRevenue: string;
  refundAmount: string;
  refundCount: number;
}

interface SubscriptionMetrics {
  newSubscriptions: number;
  canceledSubscriptions: number;
  pastDueSubscriptions: number;
}

interface PaymentMetrics {
  succeededCount: number;
  succeededAmount: string;
  failedCount: number;
  failedAmount: string;
  successRate: string;
}

interface MeteringAnomaliesMetrics {
  unreportedCount: number;
  unreportedIds: string[];
  discrepantCount: number;
  discrepantIds: string[];
}

interface HealthReportContext {
  locale: string;
  userDid: string;
  pendingConsumption: PendingConsumptionMetrics;
  financial: FinancialMetrics;
  subscription: SubscriptionMetrics;
  payment: PaymentMetrics;
  meteringAnomalies: MeteringAnomaliesMetrics;
  viewDataOverviewUrl: string;
  viewSubscriptionsUrl: string;
  viewOverdueUrl: string;
}

export class HealthReportTemplate implements BaseEmailTemplate<HealthReportContext> {
  private timeRange: { start: number; end: number };
  options: {
    timeRange: { start: number; end: number };
  };

  constructor() {
    const end = dayjs();
    const start = end.subtract(24, 'hours');
    this.timeRange = {
      start: start.unix(),
      end: end.unix(),
    };
    this.options = {
      timeRange: this.timeRange,
    };
  }

  private async getPendingConsumptionMetrics(): Promise<PendingConsumptionMetrics> {
    const { sequelize } = MeterEvent;
    if (!sequelize) {
      return { customerCount: 0, pendingAmounts: '0', exceedsThreshold: false };
    }

    const { start, end } = this.timeRange;
    const startTime = dayjs.unix(start).toISOString();
    const endTime = dayjs.unix(end).toISOString();

    // 1. Get all currency configurations + pending meter events in parallel
    const [allCurrencies, events] = await Promise.all([
      PaymentCurrency.findAll(),
      MeterEvent.findAll({
        where: {
          status: ['pending', 'requires_capture', 'requires_action'],
          created_at: {
            [Op.between]: [startTime, endTime],
          },
        },
      }),
    ]);
    const currencyMap = new Map(allCurrencies.map((c) => [c.id, c]));

    if (events.length === 0) {
      return { customerCount: 0, pendingAmounts: '0', exceedsThreshold: false };
    }

    // 3. Aggregate by user and currency (accumulated mode)
    // Structure: Map<customer_id, Map<currency_id, { total: BN }>>
    const userAggregates = new Map<string, Map<string, { total: any }>>();

    // Preload all Meter configurations to reduce database queries
    const eventNames = [...new Set(events.map((e) => e.event_name))];
    const meters = await Meter.findAll({
      where: {
        event_name: eventNames,
        status: 'active',
      },
    });
    const meterMap = new Map(meters.map((m) => [m.event_name, m]));

    for (const event of events) {
      const customerId = event.getCustomerId();
      if (customerId) {
        const meter = meterMap.get(event.event_name);
        if (!meter) {
          // eslint-disable-next-line no-continue
          continue;
        }
        const currencyId = meter?.currency_id;
        if (currencyId) {
          if (!userAggregates.has(customerId)) {
            userAggregates.set(customerId, new Map());
          }

          const currencyGroups = userAggregates.get(customerId)!;
          if (!currencyGroups.has(currencyId)) {
            currencyGroups.set(currencyId, { total: new BN('0') });
          }

          const group = currencyGroups.get(currencyId)!;
          group.total = group.total.add(new BN(event.credit_pending || '0'));
        }
      }
    }

    // 4. Calculate totals and check threshold
    const allUsers = new Set<string>();
    const currencyTotals = new Map<string, any>(); // Map<currency_id, totalBN>
    let exceedsThreshold = false;

    for (const [customerId, currencyGroups] of userAggregates) {
      allUsers.add(customerId);

      for (const [currencyId, group] of currencyGroups) {
        const currency = currencyMap.get(currencyId);
        if (currency) {
          // Accumulate to global total for this currency
          const currentTotal = currencyTotals.get(currencyId) || new BN('0');
          currencyTotals.set(currencyId, currentTotal.add(group.total));

          // Check if this user's pending exceeds threshold for this currency
          const thresholdInUnit = new BN(overdueThreshold.toString()).mul(new BN(10).pow(new BN(currency.decimal)));
          if (group.total.gt(thresholdInUnit)) {
            exceedsThreshold = true;
          }
        }
      }
    }

    if (allUsers.size === 0) {
      return { customerCount: 0, pendingAmounts: '0', exceedsThreshold: false };
    }

    // 5. Format results
    const formattedAmounts: string[] = [];
    for (const [currencyId, totalBN] of currencyTotals) {
      const currency = currencyMap.get(currencyId);
      if (currency) {
        const formattedAmount = fromUnitToToken(totalBN.toString(), currency.decimal);
        formattedAmounts.push(this.formatAmountWithoutSuffix(formattedAmount, currency.symbol));
      }
    }

    return {
      customerCount: allUsers.size,
      pendingAmounts: formattedAmounts.join(' / ') || '0',
      exceedsThreshold,
    };
  }

  private async getFinancialMetrics(): Promise<FinancialMetrics> {
    const { start, end } = this.timeRange;
    const startTime = dayjs.unix(start).toISOString();
    const endTime = dayjs.unix(end).toISOString();

    // Get all successful payments + refunds in parallel
    const [payments, refunds] = await Promise.all([
      PaymentIntent.findAll({
        where: {
          status: 'succeeded',
          created_at: { [Op.between]: [startTime, endTime] },
        },
      }),
      Refund.findAll({
        where: {
          status: 'succeeded',
          created_at: { [Op.between]: [startTime, endTime] },
        },
      }),
    ]);

    // Aggregate total revenue by currency
    const revenueByCurrency = new Map<string, BN>();
    for (const payment of payments) {
      const current = revenueByCurrency.get(payment.currency_id) || new BN('0');
      revenueByCurrency.set(payment.currency_id, current.add(new BN(payment.amount_received || '0')));
    }

    // Aggregate total refund by currency
    const refundByCurrency = new Map<string, BN>();
    for (const refund of refunds) {
      const current = refundByCurrency.get(refund.currency_id) || new BN('0');
      refundByCurrency.set(refund.currency_id, current.add(new BN(refund.amount || '0')));
    }

    // Get currency info
    const allCurrencyIds = Array.from(new Set([...revenueByCurrency.keys(), ...refundByCurrency.keys()]));
    const currencies = await PaymentCurrency.findAll({
      where: { id: { [Op.in]: allCurrencyIds } },
    });
    const currencyMap = new Map(currencies.map((c) => [c.id, c]));

    // Format revenue
    const revenueFormatted: string[] = [];
    for (const [currencyId, totalBN] of revenueByCurrency) {
      const currency = currencyMap.get(currencyId);
      if (currency) {
        const formattedAmount = fromUnitToToken(totalBN.toString(), currency.decimal);
        revenueFormatted.push(this.formatAmountWithoutSuffix(formattedAmount, currency.symbol));
      }
    }

    // Format refund
    const refundFormatted: string[] = [];
    for (const [currencyId, totalBN] of refundByCurrency) {
      const currency = currencyMap.get(currencyId);
      if (currency) {
        const formattedAmount = fromUnitToToken(totalBN.toString(), currency.decimal);
        refundFormatted.push(this.formatAmountWithoutSuffix(formattedAmount, currency.symbol));
      }
    }

    return {
      totalRevenue: revenueFormatted.join(', ') || '0',
      refundAmount: refundFormatted.join(', ') || '0',
      refundCount: refunds.length,
    };
  }

  private async getSubscriptionMetrics(): Promise<SubscriptionMetrics> {
    const { start, end } = this.timeRange;
    const startTime = dayjs.unix(start).toISOString();
    const endTime = dayjs.unix(end).toISOString();

    // All 3 counts are independent — run in parallel
    const [newSubscriptions, canceledSubscriptions, pastDueSubscriptions] = await Promise.all([
      Subscription.count({
        where: { created_at: { [Op.between]: [startTime, endTime] } },
      }),
      Subscription.count({
        where: { canceled_at: { [Op.between]: [start, end] } },
      }),
      Subscription.count({
        where: { status: 'past_due', updated_at: { [Op.between]: [startTime, endTime] } },
      }),
    ]);

    return {
      newSubscriptions,
      canceledSubscriptions,
      pastDueSubscriptions,
    };
  }

  private async getMeteringAnomaliesMetrics(): Promise<MeteringAnomaliesMetrics> {
    const { start, end } = this.timeRange;

    // Get all invoices with metered items in the last 24 hours
    const invoices = await Invoice.findAll({
      where: {
        created_at: {
          [Op.between]: [start * 1000, end * 1000],
        },
        billing_reason: {
          [Op.notIn]: ['stake', 'slash_stake', 'recharge'],
        },
      },
      include: [
        { model: Subscription, as: 'subscription' },
        { model: InvoiceItem, as: 'lines' },
      ],
    });

    const prices = (await Price.findAll()).map((x) => x.toJSON());
    const priceMap = new Map(prices.map((price) => [price.id, price]));

    const meteringInvoices: any[] = [];
    for (const invoice of invoices) {
      const invoiceJson = invoice.toJSON();
      // @ts-ignore
      (invoiceJson.lines || []).forEach((item) => {
        item.price = priceMap.get(item.price_id);
      });
      // @ts-ignore
      if ((invoiceJson.lines || []).some((line) => line.price?.recurring?.usage_type === 'metered')) {
        meteringInvoices.push(invoice);
      }
    }

    const meteringInvoiceIds = Array.from(new Set(meteringInvoices.map((invoice) => invoice.id)));
    const meteringSubscriptionIds = Array.from(
      new Set(meteringInvoices.map((invoice) => invoice?.subscription_id).filter(Boolean) as string[])
    );

    if (meteringSubscriptionIds.length === 0) {
      return { unreportedCount: 0, unreportedIds: [], discrepantCount: 0, discrepantIds: [] };
    }

    // Get unreported and discrepant events
    const [unreportedEvents, discrepantEvents] = await Promise.all([
      Event.findAll({
        where: {
          type: 'usage.report.empty',
          created_at: {
            [Op.between]: [start * 1000, end * 1000],
          },
          object_id: {
            [Op.in]: meteringSubscriptionIds,
          },
        },
      }),
      Event.findAll({
        where: {
          type: 'billing.discrepancy',
          created_at: {
            [Op.between]: [start * 1000, end * 1000],
          },
          object_id: {
            [Op.in]: meteringInvoiceIds,
          },
        },
      }),
    ]);

    const unreportedIds = Array.from(new Set(unreportedEvents.map((e) => e.object_id).filter(Boolean)));
    const discrepantIds = Array.from(
      new Set(discrepantEvents.map((e) => e.data?.object?.subscription_id).filter(Boolean))
    );

    return {
      unreportedCount: unreportedIds.length,
      unreportedIds,
      discrepantCount: discrepantIds.length,
      discrepantIds,
    };
  }

  private async getPaymentMetrics(): Promise<PaymentMetrics> {
    const { start, end } = this.timeRange;
    const startTime = dayjs.unix(start).toISOString();
    const endTime = dayjs.unix(end).toISOString();

    // Successful + failed payments in parallel
    const failedStatuses = ['canceled', 'requires_payment_method', 'requires_action'];
    const [succeededPayments, failedPayments] = await Promise.all([
      PaymentIntent.findAll({
        where: {
          status: 'succeeded',
          created_at: { [Op.between]: [startTime, endTime] },
        },
      }),
      PaymentIntent.findAll({
        where: {
          status: { [Op.in]: failedStatuses },
          created_at: { [Op.between]: [startTime, endTime] },
        },
      }),
    ]);

    const succeededCount = succeededPayments.length;
    const failedCount = failedPayments.length;
    const totalCount = succeededCount + failedCount;

    // Calculate success rate
    const successRate = totalCount > 0 ? ((succeededCount / totalCount) * 100).toFixed(2) : '100.00';

    // Aggregate amounts
    const succeededAmountByCurrency = new Map<string, BN>();
    for (const payment of succeededPayments) {
      const current = succeededAmountByCurrency.get(payment.currency_id) || new BN('0');
      succeededAmountByCurrency.set(payment.currency_id, current.add(new BN(payment.amount_received || '0')));
    }

    const failedAmountByCurrency = new Map<string, BN>();
    for (const payment of failedPayments) {
      const current = failedAmountByCurrency.get(payment.currency_id) || new BN('0');
      failedAmountByCurrency.set(payment.currency_id, current.add(new BN(payment.amount || '0')));
    }

    // Get currency info
    const allCurrencyIds = Array.from(new Set([...succeededAmountByCurrency.keys(), ...failedAmountByCurrency.keys()]));
    const currencies = await PaymentCurrency.findAll({
      where: { id: { [Op.in]: allCurrencyIds } },
    });
    const currencyMap = new Map(currencies.map((c) => [c.id, c]));

    // Format amounts
    const succeededFormatted: string[] = [];
    for (const [currencyId, totalBN] of succeededAmountByCurrency) {
      const currency = currencyMap.get(currencyId);
      if (currency) {
        const formattedAmount = fromUnitToToken(totalBN.toString(), currency.decimal);
        succeededFormatted.push(this.formatAmountWithoutSuffix(formattedAmount, currency.symbol));
      }
    }

    const failedFormatted: string[] = [];
    for (const [currencyId, totalBN] of failedAmountByCurrency) {
      const currency = currencyMap.get(currencyId);
      if (currency) {
        const formattedAmount = fromUnitToToken(totalBN.toString(), currency.decimal);
        failedFormatted.push(this.formatAmountWithoutSuffix(formattedAmount, currency.symbol));
      }
    }

    return {
      succeededCount,
      succeededAmount: succeededFormatted.join(', ') || '0',
      failedCount,
      failedAmount: failedFormatted.join(', ') || '0',
      successRate,
    };
  }

  async getContext(): Promise<HealthReportContext> {
    const pendingConsumption = await this.getPendingConsumptionMetrics();
    const financial = await this.getFinancialMetrics();
    const subscription = await this.getSubscriptionMetrics();
    const payment = await this.getPaymentMetrics();
    const meteringAnomalies = await this.getMeteringAnomaliesMetrics();

    const userDid = await getOwnerDid();
    if (!userDid) {
      throw new Error('get owner did failed');
    }
    const locale = await getUserLocale(userDid);
    const viewDataOverviewUrl = getUrl(withQuery('admin/billing', { locale }));
    const viewSubscriptionsUrl = getUrl(withQuery('admin/billing/subscriptions', { locale }));
    const viewOverdueUrl = getUrl(withQuery('admin/billing/overdue', { locale }));

    return {
      locale,
      userDid,
      pendingConsumption,
      financial,
      subscription,
      payment,
      meteringAnomalies,
      viewDataOverviewUrl,
      viewSubscriptionsUrl,
      viewOverdueUrl,
    };
  }

  private formatAmountWithoutSuffix(formattedAmount: string, currencySymbol: string | undefined): string {
    // Currency symbol mapping (same as in util.ts)
    const CURRENCY_SYMBOLS: Record<string, string> = {
      USD: '$',
      EUR: '€',
      GBP: '£',
      JPY: '¥',
      CNY: '¥',
    };

    const mappedSymbol = CURRENCY_SYMBOLS[currencySymbol || ''];
    if (mappedSymbol) {
      return `${mappedSymbol}${formattedAmount}`;
    }
    return `${formattedAmount} ${currencySymbol || ''}`;
  }

  private createTextBlock(text: string, color?: string) {
    return {
      type: 'text',
      data: {
        type: 'plain',
        ...(color ? { color } : {}),
        text,
      },
    };
  }

  private buildSectionFields(rows: Array<{ left: string; right?: string; leftColor?: string; rightColor?: string }>) {
    return rows.flatMap((row) => [
      this.createTextBlock(row.left, row.leftColor),
      this.createTextBlock(row.right ?? ' ', row.rightColor),
    ]);
  }

  async getTemplate(context?: HealthReportContext): Promise<BaseEmailTemplateType> {
    const resolvedContext = context ?? (await this.getContext());
    const {
      locale,
      pendingConsumption,
      financial,
      subscription,
      payment,
      meteringAnomalies,
      viewDataOverviewUrl,
      viewSubscriptionsUrl,
      viewOverdueUrl,
    } = resolvedContext;

    const startTimeLabel = dayjs.unix(this.timeRange.start).utc().format('YYYY-MM-DD HH:mm');
    const endTimeLabel = dayjs.unix(this.timeRange.end).utc().format('YYYY-MM-DD HH:mm');

    const labelColor = '#9397A1';
    const attentionDetailRows: Array<{ left: string; right?: string; leftColor?: string; count?: number }> = [];
    if (meteringAnomalies.unreportedCount > 0) {
      attentionDetailRows.push({
        left: translate('notification.healthReport.attentionUnreportedLabel', locale),
        right: translate('notification.healthReport.attentionUnreportedValue', locale, {
          count: meteringAnomalies.unreportedCount,
          ids: meteringAnomalies.unreportedIds.join('、'),
        }),
        leftColor: labelColor,
        count: meteringAnomalies.unreportedCount,
      });
    }
    if (meteringAnomalies.discrepantCount > 0) {
      attentionDetailRows.push({
        left: translate('notification.healthReport.attentionDiscrepantLabel', locale),
        right: translate('notification.healthReport.attentionDiscrepantValue', locale, {
          count: meteringAnomalies.discrepantCount,
          ids: meteringAnomalies.discrepantIds.join('、'),
        }),
        leftColor: labelColor,
        count: meteringAnomalies.discrepantCount,
      });
    }
    if (pendingConsumption.customerCount > 0 && pendingConsumption.exceedsThreshold) {
      attentionDetailRows.push({
        left: translate('notification.healthReport.attentionPendingConsumptionLabel', locale),
        right: translate('notification.healthReport.attentionPendingConsumptionValue', locale, {
          customers: pendingConsumption.customerCount,
          amount: pendingConsumption.pendingAmounts,
        }),
        leftColor: labelColor,
        count: pendingConsumption.customerCount,
      });
    }
    if (financial.refundCount > 0) {
      attentionDetailRows.push({
        left: translate('notification.healthReport.attentionRefundLabel', locale),
        right: translate('notification.healthReport.attentionRefundValue', locale, {
          refundCount: financial.refundCount,
          refundAmount: financial.refundAmount,
        }),
        leftColor: labelColor,
        count: financial.refundCount,
      });
    }
    if (subscription.pastDueSubscriptions > 0) {
      attentionDetailRows.push({
        left: translate('notification.healthReport.attentionPastDueLabel', locale),
        right: translate('notification.healthReport.attentionPastDueValue', locale, {
          count: subscription.pastDueSubscriptions,
        }),
        leftColor: labelColor,
        count: subscription.pastDueSubscriptions,
      });
    }

    const attentionSummary =
      attentionDetailRows.length > 0
        ? translate('notification.healthReport.attentionSummary', locale, { count: attentionDetailRows.length })
        : translate('notification.healthReport.attentionSummaryEmpty', locale);

    const attentionSectionRows = attentionDetailRows.length > 0 ? attentionDetailRows : [];

    const succeededText = translate('notification.healthReport.paymentSuccessCount', locale, {
      count: payment.succeededCount,
    });
    const failedText = translate('notification.healthReport.paymentFailedCount', locale, {
      count: payment.failedCount,
    });

    const metricsSectionRows = [
      {
        left: translate('notification.healthReport.coreRevenueLabel', locale) + ' '.repeat(100),
        right: translate('notification.healthReport.coreRevenueValue', locale, {
          revenue: financial.totalRevenue,
        }),
        leftColor: labelColor,
      },
      {
        left: translate('notification.healthReport.coreNewSubscriptionsLabel', locale),
        right: translate('notification.healthReport.coreNewSubscriptionsValue', locale, {
          count: subscription.newSubscriptions,
        }),
        leftColor: labelColor,
      },
      {
        left: translate('notification.healthReport.coreCanceledSubscriptionsLabel', locale),
        right: translate('notification.healthReport.coreCanceledSubscriptionsValue', locale, {
          count: subscription.canceledSubscriptions,
        }),
        leftColor: labelColor,
      },
      {
        left: translate('notification.healthReport.corePaymentSuccessRateLabel', locale),
        right: translate('notification.healthReport.corePaymentSuccessRateValue', locale, {
          rate: payment.successRate,
          succeeded: succeededText,
          failed: failedText,
        }),
        leftColor: labelColor,
      },
    ];

    const combinedRows = [
      { left: attentionSummary },
      ...attentionSectionRows,
      { left: translate('notification.healthReport.coreMetricsTitle', locale) },
      ...metricsSectionRows,
    ];

    const attachments = [
      {
        type: 'section',
        fields: this.buildSectionFields(combinedRows),
      },
    ];

    return {
      title: translate('notification.healthReport.title', locale, { appName: getAppName() }),
      body: `${translate('notification.healthReport.period', locale, {
        start: startTimeLabel,
        end: endTimeLabel,
      })}`,
      // @ts-expect-error
      attachments,
      // @ts-ignore
      actions: [
        viewDataOverviewUrl && {
          name: translate('notification.healthReport.viewDataOverview', locale),
          title: translate('notification.healthReport.viewDataOverview', locale),
          link: viewDataOverviewUrl,
        },
        viewSubscriptionsUrl && {
          name: translate('notification.healthReport.viewSubscriptions', locale),
          title: translate('notification.healthReport.viewSubscriptions', locale),
          link: viewSubscriptionsUrl,
        },
        viewOverdueUrl && {
          name: translate('notification.healthReport.viewOverdue', locale),
          title: translate('notification.healthReport.viewOverdue', locale),
          link: viewOverdueUrl,
        },
      ].filter(Boolean),
    };
  }
}

export async function createOverdueDetection() {
  logger.info('Start health report generation');

  try {
    const template = new HealthReportTemplate();
    const context = await template.getContext();

    const notification = await template.getTemplate(context);

    if (notification) {
      await Notification.sendToUser(context.userDid, notification as any, { allowUnsubscribe: false });
      logger.info('Health report notification sent', {
        pendingCustomers: context.pendingConsumption.customerCount,
        pendingExceedsThreshold: context.pendingConsumption.exceedsThreshold,
        pastDueSubscriptions: context.subscription.pastDueSubscriptions,
      });
    }
  } catch (error) {
    logger.error('Failed to create health report', error);
  }
}
