import { BN } from '@ocap/util';
import { Op } from 'sequelize';

import { CreditGrant, Customer, PaymentCurrency, Subscription } from '../store/models';
import { formatMetadata } from './util';
import logger from './logger';
import dayjs from './dayjs';
import { getMeterPriceIdsFromSubscription } from './subscription';

/**
 * 创建 CreditGrant - 抽象重复的创建逻辑
 * 注意：amount应该已经是Unit格式（在路由层处理fromTokenToUnit转换）
 */
export async function createCreditGrant(params: {
  amount: string; // Unit格式的金额
  currency_id?: string;
  customer_id: string;
  name?: string;
  category: 'paid' | 'promotional';
  priority?: number;
  effective_at?: number;
  expires_at?: number;
  applicability_config?: {
    scope: {
      prices?: string[];
      price_type?: 'metered';
    };
  };
  metadata?: Record<string, any>;
  livemode?: boolean;
  created_via?: string;
  created_by?: string;
}): Promise<CreditGrant> {
  const customer = await Customer.findByPk(params.customer_id);
  if (!customer) {
    throw new Error(`Customer ${params.customer_id} not found`);
  }

  const amount = new BN(params.amount);
  if (amount.lte(new BN(0))) {
    throw new Error('Amount must be greater than 0');
  }

  const currencyId = params.currency_id;
  if (!currencyId) {
    throw new Error('currency_id is required');
  }

  const now = dayjs().unix();
  if (params.expires_at && params.expires_at < now) {
    throw new Error('expires_at must be in the future');
  }
  // Always start with 'pending' status, let activateGrant handle the activation
  const initialStatus = 'pending';
  const applicabilityConfig = params.applicability_config || {
    scope: {
      type: 'metered',
    },
  };

  const grantData: any = {
    amount: params.amount,
    currency_id: currencyId,
    customer_id: params.customer_id,
    name: params.name,
    category: params.category,
    priority: params.priority || 50,
    effective_at: params.effective_at || now,
    expires_at: params.expires_at,
    applicability_config: applicabilityConfig,
    livemode: !!params.livemode,
    created_via: params.created_via || 'api',
    created_by: params.created_by,
    remaining_amount: params.amount,
    status: initialStatus,
    metadata: formatMetadata(params.metadata),
  };

  const creditGrant = await CreditGrant.create(grantData);

  logger.info('Credit grant created', {
    grantId: creditGrant.id,
    customerId: params.customer_id,
    amount: params.amount,
    currencyId,
  });

  return creditGrant;
}

export async function getCustomerCreditBalance(customerId: string, currencyId: string, subscriptionId?: string) {
  let priceIds: string[] = [];
  if (subscriptionId) {
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      throw new Error(`Subscription ${subscriptionId} not found`);
    }
    priceIds = await getMeterPriceIdsFromSubscription(subscription);
  }

  const availableGrants = await CreditGrant.getAvailableCreditsForCustomer(customerId, currencyId, priceIds);

  const totalBalance = availableGrants
    .reduce((sum, grant) => sum.add(new BN(grant.remaining_amount)), new BN(0))
    .toString();

  const paymentCurrency = await PaymentCurrency.findByPk(currencyId);

  return {
    customer_id: customerId,
    currency_id: currencyId,
    paymentCurrency,
    totalBalance,
    availableGrants,
  };
}

export function calculateExpiresAt(validDurationValue: number, validDurationUnit: string): number | undefined {
  if (!validDurationValue || validDurationValue <= 0) {
    return undefined;
  }

  const now = dayjs();
  let expiresAt: dayjs.Dayjs;

  // dayjs rounds decimal values for day/week units, so convert to mintues first
  switch (validDurationUnit) {
    case 'hours':
      expiresAt = now.add(validDurationValue * 60, 'minute');
      break;
    case 'days':
      expiresAt = now.add(validDurationValue * 24 * 60, 'minute');
      break;
    case 'weeks':
      expiresAt = now.add(validDurationValue * 7 * 24 * 60, 'minute');
      break;
    case 'months':
      expiresAt = now.add(validDurationValue, 'month');
      break;
    case 'years':
      expiresAt = now.add(validDurationValue, 'year');
      break;
    default:
      expiresAt = now.add(validDurationValue * 24 * 60, 'minute');
  }

  return expiresAt.unix();
}

/**
 * Get credit grant statistics with flexible filtering
 */
export async function getCreditGrantStats(params: {
  grantedBy?: string;
  category?: 'paid' | 'promotional';
  currencyId: string;
  startDate: number;
  endDate: number;
  timezoneOffset?: number;
}) {
  const { grantedBy, category, currencyId, startDate, endDate, timezoneOffset } = params;
  const offset = typeof timezoneOffset === 'number' ? timezoneOffset : 0;

  // Fetch currency once at the start (since currencyId is required, results will only contain this currency)
  const currency = await PaymentCurrency.findByPk(currencyId, {
    attributes: ['id', 'name', 'symbol', 'decimal'],
  });

  if (!currency) {
    return {
      stats: {
        currency_id: currencyId,
        currency: null,
        grant_count: 0,
        total_granted: '0',
        total_remaining: '0',
        total_consumed: '0',
      },
      daily_stats: [],
    };
  }

  const currencyJson = currency.toJSON();

  // Build where clause
  const where: any = {
    currency_id: currencyId,
    created_at: {
      [Op.gte]: new Date(startDate * 1000),
      [Op.lte]: new Date(endDate * 1000),
    },
  };

  if (grantedBy) {
    where['metadata.granted_by'] = grantedBy;
  }

  if (category) {
    where.category = category;
  }

  const grants = (await CreditGrant.findAll({
    where,
    attributes: ['amount', 'remaining_amount', 'created_at'],
    raw: true,
  })) as any[];

  const dailyMap = new Map<
    string,
    {
      date: string;
      grant_count: number;
      total_granted: BN;
      total_remaining: BN;
    }
  >();

  const aggregate = {
    grant_count: 0,
    total_granted: new BN(0),
    total_remaining: new BN(0),
  };

  grants.forEach((grant) => {
    const date = dayjs.utc(grant.created_at).utcOffset(offset).format('YYYY-MM-DD');
    const amount = grant.amount || '0';
    const remainingAmount = grant.remaining_amount || '0';
    if (!dailyMap.has(date)) {
      dailyMap.set(date, {
        date,
        grant_count: 0,
        total_granted: new BN(0),
        total_remaining: new BN(0),
      });
    }

    const daily = dailyMap.get(date)!;
    daily.grant_count += 1;
    daily.total_granted = daily.total_granted.add(new BN(amount));
    daily.total_remaining = daily.total_remaining.add(new BN(remainingAmount));

    aggregate.grant_count += 1;
    aggregate.total_granted = aggregate.total_granted.add(new BN(amount));
    aggregate.total_remaining = aggregate.total_remaining.add(new BN(remainingAmount));
  });

  const dailyStats = Array.from(dailyMap.values())
    .sort((a, b) => a.date.localeCompare(b.date))
    .map((day) => {
      const totalGranted = day.total_granted.toString();
      const totalRemaining = day.total_remaining.toString();
      const totalConsumed = day.total_granted.sub(day.total_remaining).toString();

      return {
        date: day.date,
        currency_id: currencyId,
        grant_count: day.grant_count,
        total_granted: totalGranted,
        total_remaining: totalRemaining,
        total_consumed: totalConsumed,
      };
    });

  const totalGranted = aggregate.total_granted.toString();
  const totalRemaining = aggregate.total_remaining.toString();
  const totalConsumed = aggregate.total_granted.sub(aggregate.total_remaining).toString();
  const statsWithConsumed = {
    currency_id: currencyId,
    currency: currencyJson,
    grant_count: aggregate.grant_count,
    total_granted: totalGranted,
    total_remaining: totalRemaining,
    total_consumed: totalConsumed,
  };

  return {
    stats: statsWithConsumed,
    daily_stats: dailyStats,
  };
}
