/* eslint-disable no-continue */
/**
 * Credit Schedule System
 *
 * Handles scheduled credit grant delivery, decoupling credit issuance from invoice billing cycles.
 * Supports flexible delivery strategies (hourly/daily/weekly/monthly intervals, trial period grants, etc.)
 */

import { fromTokenToUnit } from '@ocap/util';

import dayjs from './dayjs';
import { events } from './event';
import logger from './logger';
import { getLock } from './lock';
import { createCreditGrant, calculateExpiresAt } from './credit-grant';
import { getSubscriptionItemPrice } from './subscription';
import { CreditGrant, PaymentCurrency, Price, Product, Subscription, SubscriptionItem } from '../store/models';
import type {
  CreditConfig,
  CreditScheduleConfig,
  CreditSchedulePriceState,
  CreditScheduleState,
} from '../store/models/types';
import type { TPaymentCurrency } from '../store/models/payment-currency';

// Job types for credit schedule
export type CreditScheduleJob = {
  subscriptionId: string;
  priceId: string;
  scheduledAt: number; // The scheduled time for this grant (used for seq calculation)
  action: 'create_from_schedule';
};

/**
 * Get schedule job ID for a subscription + price + seq combination
 * Each scheduled grant has a unique job ID to avoid conflicts when
 * creating the next job while the current one is still executing
 */
export function getScheduleJobId(subscriptionId: string, priceId: string, seq: number): string {
  return `schedule-${subscriptionId}-${priceId}-${seq}`;
}

/**
 * Calculate schedule sequence number based on anchor and scheduled time
 * seq = floor((scheduled_at - schedule_anchor_at) / interval) + 1
 */
export function calculateScheduleSeq(
  anchorAt: number,
  scheduledAt: number,
  intervalValue: number,
  intervalUnit: 'hour' | 'day' | 'week' | 'month'
): number {
  if (scheduledAt < anchorAt) {
    return 0;
  }

  if (intervalUnit === 'month') {
    let seq = 1;
    let cursor = dayjs.unix(anchorAt);
    let next = cursor.add(intervalValue, 'month');

    while (next.unix() <= scheduledAt) {
      seq += 1;
      cursor = next;
      next = cursor.add(intervalValue, 'month');
    }

    return seq;
  }

  const intervalMs = getIntervalMs(intervalValue, intervalUnit, anchorAt);
  const elapsed = scheduledAt - anchorAt;
  return Math.floor(elapsed / (intervalMs / 1000)) + 1;
}

/**
 * Get interval duration in milliseconds
 * For month intervals, uses dayjs to handle variable month lengths
 */
export function getIntervalMs(
  intervalValue: number,
  intervalUnit: 'hour' | 'day' | 'week' | 'month',
  referenceTime?: number
): number {
  switch (intervalUnit) {
    case 'hour':
      return intervalValue * 60 * 60 * 1000;
    case 'day':
      return intervalValue * 24 * 60 * 60 * 1000;
    case 'week':
      return intervalValue * 7 * 24 * 60 * 60 * 1000;
    case 'month': {
      // For month, calculate based on reference time to handle variable month lengths
      const ref = referenceTime ? dayjs.unix(referenceTime) : dayjs();
      return ref.add(intervalValue, 'month').diff(ref);
    }
    default:
      return intervalValue * 24 * 60 * 60 * 1000; // Default to days
  }
}

/**
 * Calculate the next grant time based on current state and schedule config
 */
export function calculateNextGrantAt(
  anchorAt: number,
  currentSeq: number,
  intervalValue: number,
  intervalUnit: 'hour' | 'day' | 'week' | 'month'
): number {
  const anchor = dayjs.unix(anchorAt);

  // Use dayjs for month calculations to handle variable month lengths
  if (intervalUnit === 'month') {
    return anchor.add(currentSeq * intervalValue, 'month').unix();
  }

  const intervalMs = getIntervalMs(intervalValue, intervalUnit, anchorAt);
  return anchorAt + Math.floor((currentSeq * intervalMs) / 1000);
}

/**
 * Get credit config with schedule from a price
 */
export function getCreditConfigWithSchedule(
  price: any
): { creditConfig: CreditConfig; scheduleConfig: CreditScheduleConfig } | null {
  const metadata = price?.metadata || {};
  const creditConfig = metadata.credit_config as CreditConfig | undefined;

  if (!creditConfig || !creditConfig.schedule?.enabled) {
    return null;
  }

  return {
    creditConfig,
    scheduleConfig: creditConfig.schedule,
  };
}

/**
 * Calculate the first grant time based on first_grant_timing setting
 */
export function calculateFirstGrantAt(subscription: Subscription, scheduleConfig: CreditScheduleConfig): number | null {
  const now = dayjs().unix();
  const timing = scheduleConfig.first_grant_timing || 'immediate';

  switch (timing) {
    case 'immediate':
      // Grant immediately (or at subscription start if in future)
      return Math.max(now, subscription.start_date);

    case 'after_trial':
      // Grant after trial ends
      if (subscription.trial_end && subscription.trial_end > now) {
        return subscription.trial_end;
      }
      // No trial or trial ended, grant immediately
      return now;

    case 'after_first_payment':
      // Don't schedule yet; will be activated on first invoice.paid event
      return null;

    default:
      return now;
  }
}

/**
 * Calculate credit amount for a scheduled grant
 * If amount_per_grant is set, use that; otherwise divide total by period count
 */
export function calculateGrantAmount(
  creditConfig: CreditConfig,
  scheduleConfig: CreditScheduleConfig,
  currency: TPaymentCurrency
): string {
  if (scheduleConfig.amount_per_grant) {
    return fromTokenToUnit(scheduleConfig.amount_per_grant, currency.decimal).toString();
  }

  // Calculate based on billing period division
  // For simplicity, use the full credit_amount as the grant amount
  // In a real scenario, you might divide by expected grants per billing period
  return fromTokenToUnit(creditConfig.credit_amount, currency.decimal).toString();
}

/**
 * Calculate expires_at for a scheduled credit grant
 * If expire_with_next_grant is true, expires at next grant time
 * Otherwise uses the standard valid_duration settings
 */
export function calculateScheduledGrantExpiresAt(
  creditConfig: CreditConfig,
  scheduleConfig: CreditScheduleConfig,
  nextGrantAt: number
): number | undefined {
  if (scheduleConfig.expire_with_next_grant) {
    // Expire when next grant is issued
    return nextGrantAt;
  }

  // Use standard duration settings
  if (creditConfig.valid_duration_value && creditConfig.valid_duration_unit) {
    return calculateExpiresAt(creditConfig.valid_duration_value, creditConfig.valid_duration_unit);
  }

  return undefined;
}

/**
 * Initialize credit schedule state for a subscription
 *
 * Only initializes when subscription is active or trialing.
 * For subscriptions created via checkout flow (status: incomplete),
 * this should be called after subscription.started event fires.
 */
export async function initializeCreditSchedule(subscription: Subscription): Promise<CreditScheduleState | null> {
  // Only initialize for active subscriptions
  if (!subscription.isActive()) {
    logger.debug('Subscription not active, skipping credit schedule initialization', {
      subscriptionId: subscription.id,
      status: subscription.status,
    });
    return null;
  }

  // Skip if already initialized
  if (subscription.credit_schedule_state && Object.keys(subscription.credit_schedule_state).length > 0) {
    logger.debug('Credit schedule already initialized', {
      subscriptionId: subscription.id,
    });
    return subscription.credit_schedule_state;
  }

  const lock = getLock(`credit-schedule-init-${subscription.id}`);
  await lock.acquire();

  try {
    // Get subscription items with prices
    const subscriptionItems = await SubscriptionItem.findAll({
      where: { subscription_id: subscription.id },
    });

    if (subscriptionItems.length === 0) {
      logger.debug('No subscription items found for credit schedule initialization', {
        subscriptionId: subscription.id,
      });
      return null;
    }

    // Load prices with products
    const priceIds = subscriptionItems.map((item) => item.price_id);
    const prices = await Price.findAll({
      where: { id: priceIds },
      include: [{ model: Product, as: 'product' }],
    });

    const state: CreditScheduleState = {};
    const now = dayjs().unix();

    for (const price of prices) {
      // @ts-ignore - product is included
      if (price.product?.type !== 'credit') continue;

      const config = getCreditConfigWithSchedule(price);
      if (!config) continue;

      const { scheduleConfig } = config;

      // Skip if delivery_mode is 'invoice' (old behavior)
      if (scheduleConfig.delivery_mode === 'invoice') {
        continue;
      }

      // Calculate first grant time
      const firstGrantAt = calculateFirstGrantAt(subscription, scheduleConfig);

      // Initialize state for this price
      state[price.id] = {
        enabled: true,
        schedule_anchor_at: firstGrantAt || now,
        next_grant_at: firstGrantAt || 0,
        last_grant_seq: 0,
        grants_in_current_period: 0,
      };
      logger.info('Credit schedule state initialized for price', {
        subscriptionId: subscription.id,
        priceId: price.id,
        firstGrantAt,
        timing: scheduleConfig.first_grant_timing,
        deliveryMode: scheduleConfig.delivery_mode,
      });
    }

    if (Object.keys(state).length === 0) {
      return null;
    }

    // Update subscription with schedule state
    await subscription.update({ credit_schedule_state: state });

    logger.info('Credit schedule initialized for subscription', {
      subscriptionId: subscription.id,
      priceCount: Object.keys(state).length,
    });

    return state;
  } catch (error: any) {
    logger.error('Failed to initialize credit schedule', {
      subscriptionId: subscription.id,
      error: error.message,
      stack: error.stack,
    });
    throw error;
  } finally {
    lock.release();
  }
}

/**
 * Activate schedules that are waiting for the first payment
 * Called on invoice.paid when delivery_mode is schedule and first_grant_timing is after_first_payment
 */
export async function activateAfterFirstPaymentSchedules(
  subscription: Subscription,
  triggeredAt?: number
): Promise<Array<{ jobId: string; runAt: number; job: CreditScheduleJob }>> {
  if (!subscription.isActive()) {
    return [];
  }

  const lock = getLock(`credit-schedule-first-payment-${subscription.id}`);
  await lock.acquire();

  try {
    const subscriptionItems = await SubscriptionItem.findAll({
      where: { subscription_id: subscription.id },
    });

    if (subscriptionItems.length === 0) {
      return [];
    }

    const expandedItems = await Price.expand(subscriptionItems.map((item) => item.toJSON()));

    const state = subscription.credit_schedule_state || {};
    const updatedState: CreditScheduleState = { ...state };
    const jobs: Array<{ jobId: string; runAt: number; job: CreditScheduleJob }> = [];
    const now = triggeredAt || dayjs().unix();

    for (const item of expandedItems) {
      const price = getSubscriptionItemPrice(item);
      if (!price) continue;
      if (price.product?.type !== 'credit') continue;

      const config = getCreditConfigWithSchedule(price);
      if (!config) continue;

      const { scheduleConfig } = config;
      if (scheduleConfig.delivery_mode !== 'schedule') continue;
      if (scheduleConfig.first_grant_timing !== 'after_first_payment') continue;

      const priceState = state[price.id];
      if (priceState?.next_grant_at && priceState.next_grant_at > 0) {
        continue;
      }

      updatedState[price.id] = {
        enabled: true,
        schedule_anchor_at: now,
        next_grant_at: now,
        last_grant_seq: 0,
        grants_in_current_period: 0,
        last_grant_id: undefined,
        last_error: undefined,
      };

      const jobId = getScheduleJobId(subscription.id, price.id, 1);
      jobs.push({
        jobId,
        runAt: now,
        job: {
          subscriptionId: subscription.id,
          priceId: price.id,
          scheduledAt: now,
          action: 'create_from_schedule',
        },
      });
    }

    if (jobs.length > 0) {
      await subscription.update({ credit_schedule_state: updatedState });
      logger.info('Activated credit schedules after first payment', {
        subscriptionId: subscription.id,
        jobCount: jobs.length,
      });
    }

    return jobs;
  } catch (error: any) {
    logger.error('Failed to activate credit schedules after first payment', {
      subscriptionId: subscription.id,
      error: error.message,
      stack: error.stack,
    });
    throw error;
  } finally {
    lock.release();
  }
}

/**
 * Handle scheduled credit grant execution
 * Called by the queue when a scheduled job triggers
 * Returns the next job parameters if scheduling should continue
 */
export async function handleScheduledCredit(
  job: CreditScheduleJob
): Promise<{ jobId: string; runAt: number; job: CreditScheduleJob } | null> {
  const { subscriptionId, priceId, scheduledAt } = job;
  const lock = getLock(`credit-schedule-exec-${subscriptionId}-${priceId}`);
  await lock.acquire();

  try {
    // Load subscription with fresh state
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      logger.warn('Subscription not found for scheduled credit', { subscriptionId, priceId });
      return null;
    }

    // Check subscription is active
    if (!subscription.isActive()) {
      logger.info('Subscription not active, skipping scheduled credit', {
        subscriptionId,
        priceId,
        status: subscription.status,
      });
      return null;
    }

    // Get state for this price
    const state = subscription.credit_schedule_state?.[priceId];
    if (!state?.enabled) {
      logger.info('Credit schedule not enabled for price, skipping', {
        subscriptionId,
        priceId,
      });
      return null;
    }

    // Load price with product
    const price = await Price.findByPk(priceId, {
      include: [{ model: Product, as: 'product' }],
    });
    if (!price) {
      logger.warn('Price not found for scheduled credit', { subscriptionId, priceId });
      return null;
    }

    const config = getCreditConfigWithSchedule(price);
    if (!config) {
      logger.warn('Price has no schedule config', { subscriptionId, priceId });
      return null;
    }

    const { creditConfig, scheduleConfig } = config;

    // Calculate sequence for this grant
    const seq = calculateScheduleSeq(
      state.schedule_anchor_at,
      scheduledAt,
      scheduleConfig.interval_value,
      scheduleConfig.interval_unit
    );

    // Idempotency check: look for existing grant with same seq
    const existingGrant = await CreditGrant.findOne({
      where: {
        customer_id: subscription.customer_id,
        'metadata.subscription_id': subscriptionId,
        'metadata.price_id': priceId,
        'metadata.schedule_seq': seq,
      },
    });

    if (existingGrant) {
      logger.info('Credit grant already exists for this seq, skipping', {
        subscriptionId,
        priceId,
        seq,
        existingGrantId: existingGrant.id,
      });
      const now = dayjs().unix();
      let nextGrantAt = calculateNextGrantAt(
        state.schedule_anchor_at,
        seq,
        scheduleConfig.interval_value,
        scheduleConfig.interval_unit
      );

      if (nextGrantAt <= now) {
        const intervalMs = getIntervalMs(scheduleConfig.interval_value, scheduleConfig.interval_unit, now);
        nextGrantAt = now + Math.ceil(intervalMs / 1000);
      }

      const newState: CreditSchedulePriceState = {
        ...state,
        last_grant_seq: seq,
        last_grant_id: existingGrant.id,
        next_grant_at: nextGrantAt,
        last_error: undefined,
      };

      await updateScheduleState(subscription, priceId, newState);

      // Return next job params
      return getNextJobParams(subscription, priceId, newState, scheduleConfig);
    }

    // Check max_grants_per_period limit
    if (
      scheduleConfig.max_grants_per_period &&
      state.grants_in_current_period >= scheduleConfig.max_grants_per_period
    ) {
      logger.warn('Max grants per period reached, skipping', {
        subscriptionId,
        priceId,
        grantsInPeriod: state.grants_in_current_period,
        maxGrants: scheduleConfig.max_grants_per_period,
      });
      const now = dayjs().unix();
      const periodEnd = subscription.status === 'trialing' ? subscription.trial_end : subscription.current_period_end;
      let nextGrantAt = state.next_grant_at;

      if (periodEnd && periodEnd > now) {
        nextGrantAt = periodEnd;
      } else {
        const intervalMs = getIntervalMs(scheduleConfig.interval_value, scheduleConfig.interval_unit, now);
        nextGrantAt = now + Math.ceil(intervalMs / 1000);
      }

      const newState: CreditSchedulePriceState = {
        ...state,
        next_grant_at: nextGrantAt,
        last_error: undefined,
      };

      await updateScheduleState(subscription, priceId, newState);

      // Return next job params (will be checked again)
      return getNextJobParams(subscription, priceId, newState, scheduleConfig);
    }

    // Get currency
    const currency = await PaymentCurrency.findByPk(creditConfig.currency_id);
    if (!currency) {
      logger.error('Currency not found for scheduled credit', {
        subscriptionId,
        priceId,
        currencyId: creditConfig.currency_id,
      });
      await updateScheduleState(subscription, priceId, {
        ...state,
        last_error: `Currency not found: ${creditConfig.currency_id}`,
      });
      return null;
    }

    // Calculate grant amount
    const amount = calculateGrantAmount(creditConfig, scheduleConfig, currency);

    // Calculate next grant time for expiration
    const now = dayjs().unix();
    let nextGrantAt = calculateNextGrantAt(
      state.schedule_anchor_at,
      seq,
      scheduleConfig.interval_value,
      scheduleConfig.interval_unit
    );

    // If job was delayed (e.g., after service restart), nextGrantAt might be in the past
    // Recalculate based on current time if needed
    if (nextGrantAt <= now) {
      const intervalMs = getIntervalMs(scheduleConfig.interval_value, scheduleConfig.interval_unit, now);
      nextGrantAt = now + Math.ceil(intervalMs / 1000);
      logger.info('Adjusted nextGrantAt for delayed job execution', {
        subscriptionId,
        priceId,
        originalNextGrantAt: calculateNextGrantAt(
          state.schedule_anchor_at,
          seq,
          scheduleConfig.interval_value,
          scheduleConfig.interval_unit
        ),
        adjustedNextGrantAt: nextGrantAt,
        now,
      });
    }

    // Calculate expiration
    const expiresAt = calculateScheduledGrantExpiresAt(creditConfig, scheduleConfig, nextGrantAt);

    // Handle expire_with_next_grant: expire previous grants via standard flow
    if (scheduleConfig.expire_with_next_grant && state.last_grant_id) {
      const previousGrant = await CreditGrant.findByPk(state.last_grant_id);
      if (previousGrant && previousGrant.status === 'granted') {
        await previousGrant.update({ expires_at: now });
        events.emit(
          'credit-grant.queued',
          `expire-${previousGrant.id}`,
          { creditGrantId: previousGrant.id, action: 'expire' },
          { sync: false }
        );
        logger.info('Scheduled previous grant expiration for refresh', {
          subscriptionId,
          priceId,
          previousGrantId: state.last_grant_id,
          expiresAt: now,
        });
      }
    }

    // Build applicability config
    let applicabilityConfig: any = { scope: { type: 'metered' } };
    if (creditConfig.applicable_prices && creditConfig.applicable_prices.length > 0) {
      applicabilityConfig = { scope: { prices: creditConfig.applicable_prices } };
    }

    // Create the credit grant
    const creditGrant = await createCreditGrant({
      amount,
      currency_id: creditConfig.currency_id,
      customer_id: subscription.customer_id,
      // @ts-ignore
      name: price.nickname || price.product?.name,
      category: 'paid',
      priority: creditConfig.priority || 50,
      expires_at: expiresAt,
      applicability_config: applicabilityConfig,
      livemode: subscription.livemode,
      created_via: 'api',
      metadata: {
        subscription_id: subscriptionId,
        price_id: priceId,
        schedule_seq: seq,
        scheduled_at: scheduledAt,
        executed_at: dayjs().unix(),
        delivery_mode: 'schedule',
      },
    });

    logger.info('Scheduled credit grant created', {
      subscriptionId,
      priceId,
      creditGrantId: creditGrant.id,
      seq,
      amount,
      expiresAt,
    });

    // Update state
    const newState: CreditSchedulePriceState = {
      ...state,
      last_grant_seq: seq,
      last_grant_id: creditGrant.id,
      grants_in_current_period: state.grants_in_current_period + 1,
      next_grant_at: nextGrantAt,
      last_error: undefined,
    };

    await updateScheduleState(subscription, priceId, newState);

    // Return next job params
    return getNextJobParams(subscription, priceId, newState, scheduleConfig);
  } catch (error: any) {
    logger.error('Failed to handle scheduled credit', {
      subscriptionId,
      priceId,
      scheduledAt,
      error: error.message,
      stack: error.stack,
    });

    // Update state with error
    const subscription = await Subscription.findByPk(subscriptionId);
    if (subscription) {
      const state = subscription.credit_schedule_state?.[priceId];
      if (state) {
        await updateScheduleState(subscription, priceId, {
          ...state,
          last_error: error.message,
        });
      }
    }

    throw error;
  } finally {
    lock.release();
  }
}

/**
 * Get next job parameters for scheduling
 * Returns the job parameters for the queue (synchronous)
 */
function getNextJobParams(
  subscription: Subscription,
  priceId: string,
  state: CreditSchedulePriceState,
  scheduleConfig: CreditScheduleConfig
): { jobId: string; runAt: number; job: CreditScheduleJob } | null {
  if (!state.enabled || !subscription.isActive()) {
    return null;
  }

  const nextGrantAt =
    state.next_grant_at ||
    calculateNextGrantAt(
      state.schedule_anchor_at,
      state.last_grant_seq,
      scheduleConfig.interval_value,
      scheduleConfig.interval_unit
    );

  // Check if subscription will end before next grant
  if (subscription.cancel_at && subscription.cancel_at < nextGrantAt) {
    logger.info('Subscription will be canceled before next grant, not scheduling', {
      subscriptionId: subscription.id,
      priceId,
      nextGrantAt,
      cancelAt: subscription.cancel_at,
    });
    return null;
  }

  // Next job seq is current seq + 1
  const nextSeq = state.last_grant_seq + 1;
  const jobId = getScheduleJobId(subscription.id, priceId, nextSeq);
  const job: CreditScheduleJob = {
    subscriptionId: subscription.id,
    priceId,
    scheduledAt: nextGrantAt,
    action: 'create_from_schedule',
  };

  logger.info('Scheduling next credit grant', {
    subscriptionId: subscription.id,
    priceId,
    jobId,
    seq: nextSeq,
    runAt: nextGrantAt,
    scheduledAt: dayjs.unix(nextGrantAt).format(),
  });

  return { jobId, runAt: nextGrantAt, job };
}

/**
 * Update schedule state for a specific price
 */
async function updateScheduleState(
  subscription: Subscription,
  priceId: string,
  newPriceState: CreditSchedulePriceState
): Promise<void> {
  const currentState = subscription.credit_schedule_state || {};
  const updatedState: CreditScheduleState = {
    ...currentState,
    [priceId]: newPriceState,
  };

  await subscription.update({ credit_schedule_state: updatedState });
}

/**
 * Stop credit schedule for a subscription
 * Called when subscription is canceled/deleted
 */
export async function stopCreditSchedule(subscription: Subscription): Promise<string[]> {
  const state = subscription.credit_schedule_state;
  if (!state || Object.keys(state).length === 0) {
    return [];
  }

  const jobIds: string[] = [];

  // Collect job IDs to be deleted (next pending job for each price)
  for (const priceId of Object.keys(state)) {
    const priceState = state[priceId];
    if (priceState?.enabled) {
      // The pending job has seq = last_grant_seq + 1
      const nextSeq = (priceState.last_grant_seq || 0) + 1;
      jobIds.push(getScheduleJobId(subscription.id, priceId, nextSeq));
    }
  }

  // Clear the schedule state
  await subscription.update({ credit_schedule_state: undefined });

  logger.info('Credit schedule stopped for subscription', {
    subscriptionId: subscription.id,
    jobIds,
  });

  return jobIds;
}

/**
 * Check if a subscription has any active credit schedules
 */
export function hasActiveCreditSchedule(subscription: Subscription): boolean {
  const state = subscription.credit_schedule_state;
  if (!state) {
    return false;
  }

  return Object.values(state).some((priceState) => priceState.enabled && priceState.next_grant_at > 0);
}

/**
 * Get all subscriptions with active credit schedules
 * Used for recovery on system restart
 */
export function getSubscriptionsWithCreditSchedule(): Promise<Subscription[]> {
  return Subscription.findAll({
    where: {
      status: ['active', 'trialing'],
      credit_schedule_state: {
        // SQLite/Sequelize JSON query - check that field exists and is not null
        // This is a simplified check; actual implementation may need adjustment based on database
      },
    },
  });
}

/**
 * Reset grants_in_current_period counter
 * Should be called at the start of each billing period
 */
export async function resetPeriodGrantCounter(subscription: Subscription): Promise<void> {
  const state = subscription.credit_schedule_state;
  if (!state) {
    return;
  }

  const updatedState: CreditScheduleState = {};

  for (const priceId of Object.keys(state)) {
    const priceState = state[priceId];
    if (priceState) {
      updatedState[priceId] = {
        ...priceState,
        grants_in_current_period: 0,
      };
    }
  }

  await subscription.update({ credit_schedule_state: updatedState });

  logger.info('Reset period grant counters', {
    subscriptionId: subscription.id,
  });
}
