import { db } from '@sim/db'
import { member, organization, settings, user, userStats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import {
  getEmailSubject,
  renderCreditsExhaustedEmail,
  renderFreeTierUpgradeEmail,
  renderUsageThresholdEmail,
} from '@/components/emails'
import { getEffectiveBillingStatus } from '@/lib/billing/core/access'
import { defaultBillingPeriod } from '@/lib/billing/core/billing-period'
import {
  getHighestPrioritySubscription,
  type HighestPrioritySubscription,
} from '@/lib/billing/core/plan'
import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log'
import {
  computeDailyRefreshConsumed,
  getOrgMemberRefreshBounds,
} from '@/lib/billing/credits/daily-refresh'
import { getPlanTierDollars, isEnterprise, isFree, isPaid, isPro } from '@/lib/billing/plan-helpers'
import {
  canEditUsageLimit,
  getFreeTierLimit,
  getPerUserMinimumLimit,
  getPlanPricing,
  hasPaidSubscriptionStatus,
  hasUsableSubscriptionAccess,
  isOrgScopedSubscription,
} from '@/lib/billing/subscriptions/utils'
import type { BillingData, UsageData, UsageLimitInfo } from '@/lib/billing/types'
import { buildUpgradeHref } from '@/lib/billing/upgrade-reasons'
import { Decimal, toDecimal, toNumber } from '@/lib/billing/utils/decimal'
import { isBillingEnabled } from '@/lib/core/config/env-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import type { DbClient } from '@/lib/db/types'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getEmailPreferences } from '@/lib/messaging/email/unsubscribe'

const logger = createLogger('UsageManagement')

export interface OrgUsageLimitResult {
  limit: number
  minimum: number
}

/**
 * Sum `currentPeriodCost` across all members of an organization.
 * The single source of truth for pooled-usage reads so every caller
 * applies identical null-handling and query shape. Does NOT apply
 * daily-refresh deduction — callers layer that on top themselves
 * because refresh math needs the caller's `sub` context (plan,
 * period, seats, per-user bounds).
 *
 * Uses `LEFT JOIN` so members whose `userStats` row is missing still
 * appear (contributing 0), which keeps `memberIds` complete for
 * downstream refresh / bounds computations.
 */
export async function getPooledOrgCurrentPeriodCost(
  organizationId: string,
  executor: DbClient = db
): Promise<{ memberIds: string[]; currentPeriodCost: number; lastPeriodCost: number }> {
  const rows = await executor
    .select({
      userId: member.userId,
      currentPeriodCost: userStats.currentPeriodCost,
      lastPeriodCost: userStats.lastPeriodCost,
    })
    .from(member)
    .leftJoin(userStats, eq(member.userId, userStats.userId))
    .where(eq(member.organizationId, organizationId))

  let pooled = new Decimal(0)
  let lastPeriodCost = new Decimal(0)
  const memberIds: string[] = []
  for (const row of rows) {
    memberIds.push(row.userId)
    pooled = pooled.plus(toDecimal(row.currentPeriodCost))
    lastPeriodCost = lastPeriodCost.plus(toDecimal(row.lastPeriodCost))
  }

  return {
    memberIds,
    currentPeriodCost: toNumber(pooled),
    lastPeriodCost: toNumber(lastPeriodCost),
  }
}

/**
 * Calculates the effective usage limit for an organization-scoped plan.
 * Enterprise uses the configured orgUsageLimit directly; every other
 * paid plan uses `basePrice × seats` (Stripe's `price × quantity`) as a
 * floor. Returns `{ limit, minimum }` where `limit = max(configured, minimum)`.
 */
export async function getOrgUsageLimit(
  organizationId: string,
  plan: string,
  seats: number | null,
  executor: DbClient = db
): Promise<OrgUsageLimitResult> {
  const orgData = await executor
    .select({ orgUsageLimit: organization.orgUsageLimit })
    .from(organization)
    .where(eq(organization.id, organizationId))
    .limit(1)

  const configured =
    orgData.length > 0 && orgData[0].orgUsageLimit
      ? toNumber(toDecimal(orgData[0].orgUsageLimit))
      : null

  if (isEnterprise(plan)) {
    // Enterprise: Use configured limit directly (no per-seat minimum)
    if (configured !== null) {
      return { limit: configured, minimum: configured }
    }
    logger.warn('Enterprise org missing usage limit', { orgId: organizationId })
    return { limit: 0, minimum: 0 }
  }

  const { basePrice } = getPlanPricing(plan)
  // `||` not `??` — 0 is never a valid seat count for a paid sub.
  const seatCount = seats || 1
  const minimum = seatCount * basePrice

  if (configured !== null) {
    return { limit: Math.max(configured, minimum), minimum }
  }

  logger.warn('Org missing usage limit, using plan-driven minimum as fallback', {
    orgId: organizationId,
    plan,
    seats: seatCount,
    minimum,
  })
  return { limit: minimum, minimum }
}

/**
 * Handle new user setup when they join the platform
 * Creates userStats record with default free credits
 */
export async function handleNewUser(userId: string): Promise<void> {
  try {
    await db.insert(userStats).values({
      id: generateId(),
      userId: userId,
      currentUsageLimit: getFreeTierLimit().toString(),
      usageLimitUpdatedAt: new Date(),
    })

    logger.info('User stats record created for new user', { userId })
  } catch (error) {
    logger.error('Failed to create user stats record for new user', {
      userId,
      error,
    })
    throw error
  }
}

/**
 * Ensures a userStats record exists for a user.
 * Creates one with default values if missing.
 * This is a fallback for cases where the user.create.after hook didn't fire
 * (e.g., OAuth account linking to existing users).
 *
 * Always writes to the primary — never takes a read-routing executor.
 */
export async function ensureUserStatsExists(userId: string): Promise<void> {
  await db
    .insert(userStats)
    .values({
      id: generateId(),
      userId: userId,
      currentUsageLimit: getFreeTierLimit().toString(),
      usageLimitUpdatedAt: new Date(),
    })
    .onConflictDoNothing({ target: userStats.userId })
}

/**
 * Get comprehensive usage data for a user
 */
export async function getUserUsageData(
  userId: string,
  executor: DbClient = db
): Promise<UsageData> {
  try {
    // Write — always on the primary regardless of executor routing.
    await ensureUserStatsExists(userId)

    const [userStatsData, subscription] = await Promise.all([
      // Read-your-write: must see the row ensureUserStatsExists may have just
      // inserted, which a lagging replica can miss (this path throws on a
      // missing row). Stays on the primary deliberately.
      db
        .select()
        .from(userStats)
        .where(eq(userStats.userId, userId))
        .limit(1),
      getHighestPrioritySubscription(userId, { executor }),
    ])

    if (userStatsData.length === 0) {
      logger.error('User stats not found for userId', { userId })
      throw new Error(`User stats not found for userId: ${userId}`)
    }

    const stats = userStatsData[0]
    const orgScoped = isOrgScopedSubscription(subscription, userId)
    const billingPeriod =
      subscription?.periodStart && subscription.periodEnd
        ? { start: subscription.periodStart, end: subscription.periodEnd }
        : defaultBillingPeriod()

    let currentUsageDecimal = toDecimal(stats.currentPeriodCost)
    if (!orgScoped) {
      currentUsageDecimal = currentUsageDecimal.plus(
        await getBillingPeriodUsageCost(
          { type: 'user', id: userId },
          billingPeriod,
          undefined,
          executor
        )
      )
    }

    // For personally-scoped Pro users, include any snapshotted usage from
    // a prior org-join so the display reflects their total Pro usage.
    if (subscription && isPro(subscription.plan) && !orgScoped) {
      const snapshotUsageDecimal = toDecimal(stats.proPeriodCostSnapshot)
      if (snapshotUsageDecimal.greaterThan(0)) {
        currentUsageDecimal = currentUsageDecimal.plus(snapshotUsageDecimal)
        logger.info('Including Pro snapshot in usage display', {
          userId,
          currentPeriodCost: stats.currentPeriodCost,
          proPeriodCostSnapshot: toNumber(snapshotUsageDecimal),
          totalUsage: toNumber(currentUsageDecimal),
        })
      }
    }
    let currentUsage = toNumber(currentUsageDecimal)
    let lastPeriodCost = toNumber(toDecimal(stats.lastPeriodCost))

    let limit: number
    // Shared between the pooled-usage and pooled-refresh blocks so we
    // don't issue the member lookup twice per org-scoped call.
    let orgMemberIds: string[] = []

    if (orgScoped && subscription) {
      const orgLimit = await getOrgUsageLimit(
        subscription.referenceId,
        subscription.plan,
        subscription.seats,
        executor
      )
      limit = orgLimit.limit

      const pooled = await getPooledOrgCurrentPeriodCost(subscription.referenceId, executor)
      orgMemberIds = pooled.memberIds
      lastPeriodCost = pooled.lastPeriodCost
      const ledgerUsage = await getBillingPeriodUsageCost(
        { type: 'organization', id: subscription.referenceId },
        billingPeriod,
        undefined,
        executor
      )
      currentUsage = pooled.currentPeriodCost + ledgerUsage
    } else {
      limit = stats.currentUsageLimit
        ? toNumber(toDecimal(stats.currentUsageLimit))
        : getFreeTierLimit()
    }

    const billingPeriodStart = subscription?.periodStart ?? null
    const billingPeriodEnd = subscription?.periodEnd ?? null

    let dailyRefreshConsumed = 0
    if (subscription && isPaid(subscription.plan) && billingPeriodStart) {
      const planDollars = getPlanTierDollars(subscription.plan)
      if (planDollars > 0) {
        if (orgScoped) {
          if (orgMemberIds.length > 0) {
            const userBounds = await getOrgMemberRefreshBounds(
              subscription.referenceId,
              billingPeriodStart,
              executor
            )
            dailyRefreshConsumed = await computeDailyRefreshConsumed(
              {
                userIds: orgMemberIds,
                periodStart: billingPeriodStart,
                periodEnd: billingPeriodEnd,
                planDollars,
                seats: subscription.seats || 1,
                userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined,
                billingEntity: { type: 'organization', id: subscription.referenceId },
              },
              executor
            )
          }
        } else {
          dailyRefreshConsumed = await computeDailyRefreshConsumed(
            {
              userIds: [userId],
              periodStart: billingPeriodStart,
              periodEnd: billingPeriodEnd,
              planDollars,
              billingEntity: { type: 'user', id: userId },
            },
            executor
          )
        }
      }
    }

    const effectiveUsage = Math.max(0, currentUsage - dailyRefreshConsumed)
    const percentUsed = limit > 0 ? Math.min((effectiveUsage / limit) * 100, 100) : 0
    const isWarning = percentUsed >= 80
    const isExceeded = effectiveUsage >= limit

    return {
      currentUsage: effectiveUsage,
      limit,
      percentUsed,
      isWarning,
      isExceeded,
      billingPeriodStart,
      billingPeriodEnd,
      lastPeriodCost,
    }
  } catch (error) {
    logger.error('Failed to get user usage data', { userId, error })
    throw error
  }
}

/**
 * Get usage limit information for a user
 */
export async function getUserUsageLimitInfo(userId: string): Promise<UsageLimitInfo> {
  try {
    const [subscription, userStatsRecord] = await Promise.all([
      getHighestPrioritySubscription(userId),
      db.select().from(userStats).where(eq(userStats.userId, userId)).limit(1),
    ])

    if (userStatsRecord.length === 0) {
      throw new Error(`User stats not found for userId: ${userId}`)
    }

    const stats = userStatsRecord[0]
    const orgScoped = isOrgScopedSubscription(subscription, userId)

    let currentLimit: number
    let minimumLimit: number
    let canEdit: boolean

    if (orgScoped && subscription) {
      const orgLimit = await getOrgUsageLimit(
        subscription.referenceId,
        subscription.plan,
        subscription.seats
      )
      currentLimit = orgLimit.limit
      minimumLimit = orgLimit.minimum
      canEdit = false
    } else {
      currentLimit = stats.currentUsageLimit
        ? toNumber(toDecimal(stats.currentUsageLimit))
        : getFreeTierLimit()
      minimumLimit = getPerUserMinimumLimit(subscription)
      canEdit = canEditUsageLimit(subscription)
    }

    return {
      currentLimit,
      canEdit,
      minimumLimit,
      plan: subscription?.plan || 'free',
      updatedAt: stats.usageLimitUpdatedAt,
      scope: orgScoped ? 'organization' : 'user',
      organizationId: orgScoped && subscription ? subscription.referenceId : null,
    }
  } catch (error) {
    logger.error('Failed to get usage limit info', { userId, error })
    throw error
  }
}

/**
 * Initialize usage limits for a new user
 */
async function initializeUserUsageLimit(userId: string): Promise<void> {
  // Check if user already has usage stats
  const existingStats = await db
    .select()
    .from(userStats)
    .where(eq(userStats.userId, userId))
    .limit(1)

  if (existingStats.length > 0) {
    return
  }

  const subscription = await getHighestPrioritySubscription(userId)
  const orgScoped = isOrgScopedSubscription(subscription, userId)

  await db.insert(userStats).values({
    id: generateId(),
    userId,
    currentUsageLimit: orgScoped ? null : getFreeTierLimit().toString(),
    usageLimitUpdatedAt: new Date(),
  })

  logger.info('Initialized user stats', {
    userId,
    plan: subscription?.plan || 'free',
    hasIndividualLimit: !orgScoped,
  })
}

/**
 * Update a user's custom usage limit
 */
export async function updateUserUsageLimit(
  userId: string,
  newLimit: number,
  setBy?: string // For team admin tracking
): Promise<{ success: boolean; error?: string }> {
  try {
    const subscription = await getHighestPrioritySubscription(userId)

    if (isOrgScopedSubscription(subscription, userId)) {
      return {
        success: false,
        error:
          'This subscription is managed at the organization level. Update the organization usage limit instead.',
      }
    }

    // Only pro users can edit limits (free users cannot)
    if (!subscription || isFree(subscription.plan)) {
      return { success: false, error: 'Free plan users cannot edit usage limits' }
    }

    const billingStatus = await getEffectiveBillingStatus(userId)
    if (!hasUsableSubscriptionAccess(subscription.status, billingStatus.billingBlocked)) {
      return { success: false, error: 'An active subscription is required to edit usage limits' }
    }

    const minimumLimit = getPerUserMinimumLimit(subscription)

    logger.info('Applying plan-based validation', {
      userId,
      newLimit,
      minimumLimit,
      plan: subscription?.plan,
    })

    // Validate new limit is not below minimum
    if (newLimit < minimumLimit) {
      return {
        success: false,
        error: `Usage limit cannot be below plan minimum of $${minimumLimit}`,
      }
    }

    await db
      .update(userStats)
      .set({
        currentUsageLimit: newLimit.toString(),
        usageLimitUpdatedAt: new Date(),
      })
      .where(eq(userStats.userId, userId))

    logger.info('Updated user usage limit', {
      userId,
      newLimit,
      setBy: setBy || userId,
      planMinimum: minimumLimit,
      plan: subscription?.plan,
    })

    return { success: true }
  } catch (error) {
    logger.error('Failed to update usage limit', { userId, newLimit, error })
    return { success: false, error: 'Failed to update usage limit' }
  }
}

/**
 * Get usage limit for a user (used by checkUsageStatus for server-side
 * checks). Org-scoped subs return the organization limit;
 * personally-scoped subs return the individual user limit from userStats.
 *
 * Org-scoped members carry a null `currentUsageLimit` by design (see
 * `syncUsageLimitsFromSubscription`). A user whose subscription stops being
 * org-scoped without a resync would otherwise stay null and fail closed on
 * every execution, so a null limit self-heals to the plan default here. The
 * write-back is best-effort: a limit written concurrently wins, and a failed
 * write still resolves to the fallback instead of blocking execution.
 */
export async function getUserUsageLimit(
  userId: string,
  preloadedSubscription?: HighestPrioritySubscription
): Promise<number> {
  const subscription =
    preloadedSubscription !== undefined
      ? preloadedSubscription
      : await getHighestPrioritySubscription(userId)

  if (isOrgScopedSubscription(subscription, userId) && subscription) {
    const orgExists = await db
      .select({ id: organization.id })
      .from(organization)
      .where(eq(organization.id, subscription.referenceId))
      .limit(1)

    if (orgExists.length === 0) {
      throw new Error(`Organization not found: ${subscription.referenceId} for user: ${userId}`)
    }

    const orgLimit = await getOrgUsageLimit(
      subscription.referenceId,
      subscription.plan,
      subscription.seats
    )
    return orgLimit.limit
  }

  const userStatsQuery = await db
    .select({ currentUsageLimit: userStats.currentUsageLimit })
    .from(userStats)
    .where(eq(userStats.userId, userId))
    .limit(1)

  if (userStatsQuery.length === 0) {
    throw new Error(
      `No user stats record found for userId: ${userId}. User must be properly initialized before execution.`
    )
  }

  if (!userStatsQuery[0].currentUsageLimit) {
    const fallbackLimit =
      subscription && hasPaidSubscriptionStatus(subscription.status)
        ? getPerUserMinimumLimit(subscription)
        : getFreeTierLimit()

    try {
      const healed = await db
        .update(userStats)
        .set({
          currentUsageLimit: fallbackLimit.toString(),
          usageLimitUpdatedAt: new Date(),
        })
        .where(and(eq(userStats.userId, userId), isNull(userStats.currentUsageLimit)))
        .returning({ currentUsageLimit: userStats.currentUsageLimit })

      if (healed.length === 0) {
        const concurrent = await db
          .select({ currentUsageLimit: userStats.currentUsageLimit })
          .from(userStats)
          .where(eq(userStats.userId, userId))
          .limit(1)

        if (concurrent[0]?.currentUsageLimit) {
          return toNumber(toDecimal(concurrent[0].currentUsageLimit))
        }
      }

      logger.warn('Healed null usage limit to plan default', {
        userId,
        plan: subscription?.plan || 'free',
        fallbackLimit,
      })
    } catch (error) {
      logger.error('Failed to heal null usage limit', { userId, fallbackLimit, error })
    }

    return fallbackLimit
  }

  return toNumber(toDecimal(userStatsQuery[0].currentUsageLimit))
}

/**
 * Check usage status with warning thresholds
 */
export async function checkUsageStatus(userId: string): Promise<{
  status: 'ok' | 'warning' | 'exceeded'
  usageData: UsageData
}> {
  try {
    const usageData = await getUserUsageData(userId)

    let status: 'ok' | 'warning' | 'exceeded' = 'ok'
    if (usageData.isExceeded) {
      status = 'exceeded'
    } else if (usageData.isWarning) {
      status = 'warning'
    }

    return {
      status,
      usageData,
    }
  } catch (error) {
    logger.error('Failed to check usage status', { userId, error })
    throw error
  }
}

/**
 * Sync usage limits based on subscription changes
 */
export async function syncUsageLimitsFromSubscription(userId: string): Promise<void> {
  const [subscription, currentUserStats] = await Promise.all([
    getHighestPrioritySubscription(userId),
    db.select().from(userStats).where(eq(userStats.userId, userId)).limit(1),
  ])

  if (currentUserStats.length === 0) {
    throw new Error(`User stats not found for userId: ${userId}`)
  }

  const currentStats = currentUserStats[0]

  if (isOrgScopedSubscription(subscription, userId)) {
    if (currentStats.currentUsageLimit !== null) {
      await db
        .update(userStats)
        .set({
          currentUsageLimit: null,
          usageLimitUpdatedAt: new Date(),
        })
        .where(eq(userStats.userId, userId))

      logger.info('Cleared individual limit for org-scoped member', {
        userId,
        plan: subscription?.plan,
      })
    }
    return
  }
  const defaultLimit = getPerUserMinimumLimit(subscription)
  const currentLimit = currentStats.currentUsageLimit
    ? toNumber(toDecimal(currentStats.currentUsageLimit))
    : 0

  if (!subscription || !hasPaidSubscriptionStatus(subscription.status)) {
    // Downgraded to free
    await db
      .update(userStats)
      .set({
        currentUsageLimit: getFreeTierLimit().toString(),
        usageLimitUpdatedAt: new Date(),
      })
      .where(eq(userStats.userId, userId))

    logger.info('Set limit to free tier', { userId })
  } else if (currentLimit < defaultLimit) {
    await db
      .update(userStats)
      .set({
        currentUsageLimit: defaultLimit.toString(),
        usageLimitUpdatedAt: new Date(),
      })
      .where(eq(userStats.userId, userId))

    logger.info('Raised limit to plan minimum', {
      userId,
      newLimit: defaultLimit,
    })
  }
  // Keep higher custom limits unchanged
}

/**
 * Returns the effective current period usage cost for a user, with daily
 * refresh credits deducted. Org-scoped subs return the pooled sum across
 * all org members; personally-scoped subs return this user's own cost.
 */
export async function getEffectiveCurrentPeriodCost(
  userId: string,
  executor: DbClient = db
): Promise<number> {
  const subscription = await getHighestPrioritySubscription(userId, { executor })
  const orgScoped = isOrgScopedSubscription(subscription, userId)

  let rawCost: number
  let refreshUserIds: string[] = [userId]

  if (orgScoped && subscription) {
    const pooled = await getPooledOrgCurrentPeriodCost(subscription.referenceId, executor)
    if (pooled.memberIds.length === 0) return 0
    refreshUserIds = pooled.memberIds
    const billingPeriod =
      subscription.periodStart && subscription.periodEnd
        ? { start: subscription.periodStart, end: subscription.periodEnd }
        : defaultBillingPeriod()
    rawCost =
      pooled.currentPeriodCost +
      (await getBillingPeriodUsageCost(
        { type: 'organization', id: subscription.referenceId },
        billingPeriod,
        undefined,
        executor
      ))
  } else {
    const rows = await executor
      .select({ current: userStats.currentPeriodCost })
      .from(userStats)
      .where(eq(userStats.userId, userId))
      .limit(1)

    if (rows.length === 0) return 0
    const billingPeriod =
      subscription?.periodStart && subscription.periodEnd
        ? { start: subscription.periodStart, end: subscription.periodEnd }
        : defaultBillingPeriod()
    rawCost =
      toNumber(toDecimal(rows[0].current)) +
      (await getBillingPeriodUsageCost(
        { type: 'user', id: userId },
        billingPeriod,
        undefined,
        executor
      ))
  }

  if (!subscription || !isPaid(subscription.plan) || !subscription.periodStart) {
    return rawCost
  }

  const planDollars = getPlanTierDollars(subscription.plan)
  if (planDollars <= 0) return rawCost

  const userBounds =
    orgScoped && subscription.periodStart
      ? await getOrgMemberRefreshBounds(
          subscription.referenceId,
          subscription.periodStart,
          executor
        )
      : {}

  const refreshConsumed = await computeDailyRefreshConsumed(
    {
      userIds: refreshUserIds,
      periodStart: subscription.periodStart,
      periodEnd: subscription.periodEnd ?? null,
      planDollars,
      seats: subscription.seats || 1,
      userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined,
      billingEntity:
        orgScoped && subscription
          ? { type: 'organization', id: subscription.referenceId }
          : { type: 'user', id: userId },
    },
    executor
  )

  return Math.max(0, rawCost - refreshConsumed)
}

/**
 * Calculate billing projection based on current usage
 */
async function calculateBillingProjection(userId: string): Promise<BillingData> {
  try {
    const usageData = await getUserUsageData(userId)

    if (!usageData.billingPeriodStart || !usageData.billingPeriodEnd) {
      return {
        currentPeriodCost: usageData.currentUsage,
        projectedCost: usageData.currentUsage,
        limit: usageData.limit,
        billingPeriodStart: null,
        billingPeriodEnd: null,
        daysRemaining: 0,
      }
    }

    const now = new Date()
    const periodStart = new Date(usageData.billingPeriodStart)
    const periodEnd = new Date(usageData.billingPeriodEnd)

    const totalDays = Math.ceil(
      (periodEnd.getTime() - periodStart.getTime()) / (1000 * 60 * 60 * 24)
    )
    const daysElapsed = Math.ceil((now.getTime() - periodStart.getTime()) / (1000 * 60 * 60 * 24))
    const daysRemaining = Math.max(0, totalDays - daysElapsed)

    // Project cost based on daily usage rate
    const dailyRate = daysElapsed > 0 ? usageData.currentUsage / daysElapsed : 0
    const projectedCost = dailyRate * totalDays

    return {
      currentPeriodCost: usageData.currentUsage,
      projectedCost: Math.min(projectedCost, usageData.limit), // Cap at limit
      limit: usageData.limit,
      billingPeriodStart: usageData.billingPeriodStart,
      billingPeriodEnd: usageData.billingPeriodEnd,
      daysRemaining,
    }
  } catch (error) {
    logger.error('Failed to calculate billing projection', { userId, error })
    throw error
  }
}

/**
 * Send usage threshold notification when crossing from <80% to ≥80%.
 * - Skips when billing is disabled.
 * - Respects user-level notifications toggle and unsubscribe preferences.
 * - For organization plans, emails owners/admins who have notifications enabled.
 */
export async function maybeSendUsageThresholdEmail(params: {
  scope: 'user' | 'organization'
  planName: string
  percentBefore: number
  percentAfter: number
  userId?: string
  userEmail?: string
  userName?: string
  organizationId?: string
  /** Workspace the usage occurred in, used to build a live upgrade/billing link. */
  workspaceId?: string
  currentUsageAfter: number
  limit: number
}): Promise<void> {
  try {
    if (!isBillingEnabled) return
    if (params.limit <= 0 || params.currentUsageAfter <= 0) return

    const baseUrl = getBaseUrl()
    const isFreeUser = params.planName === 'Free'

    const upgradeCreditsLink = params.workspaceId
      ? `${baseUrl}${buildUpgradeHref(params.workspaceId, 'credits')}`
      : `${baseUrl}/workspace`
    const billingSettingsLink = params.workspaceId
      ? `${baseUrl}/workspace/${params.workspaceId}/settings/billing`
      : `${baseUrl}/workspace`

    // Check for 80% threshold crossing — used for paid users (budget warning) and free users (upgrade nudge)
    const crosses80 = params.percentBefore < 80 && params.percentAfter >= 80
    // Check for 100% threshold (free users only — credits exhausted)
    const crosses100 = params.percentBefore < 100 && params.percentAfter >= 100

    // Skip if no thresholds crossed
    if (!crosses80 && !crosses100) return

    // For 80% threshold email (paid users only)
    if (crosses80 && !isFreeUser) {
      const ctaLink = billingSettingsLink
      const sendTo = async (email: string, name?: string) => {
        const prefs = await getEmailPreferences(email)
        if (prefs?.unsubscribeAll || prefs?.unsubscribeNotifications) return

        const html = await renderUsageThresholdEmail({
          userName: name,
          planName: params.planName,
          percentUsed: Math.min(100, Math.round(params.percentAfter)),
          currentUsage: params.currentUsageAfter,
          limit: params.limit,
          ctaLink,
        })

        await sendEmail({
          to: email,
          subject: getEmailSubject('usage-threshold'),
          html,
          emailType: 'notifications',
        })
      }

      if (params.scope === 'user' && params.userId && params.userEmail) {
        const rows = await db
          .select({ enabled: settings.billingUsageNotificationsEnabled })
          .from(settings)
          .where(eq(settings.userId, params.userId))
          .limit(1)
        if (rows.length > 0 && rows[0].enabled === false) return
        await sendTo(params.userEmail, params.userName)
      } else if (params.scope === 'organization' && params.organizationId) {
        const admins = await db
          .select({
            email: user.email,
            name: user.name,
            enabled: settings.billingUsageNotificationsEnabled,
            role: member.role,
          })
          .from(member)
          .innerJoin(user, eq(member.userId, user.id))
          .leftJoin(settings, eq(settings.userId, member.userId))
          .where(eq(member.organizationId, params.organizationId))

        for (const a of admins) {
          const isAdmin = isOrgAdminRole(a.role)
          if (!isAdmin) continue
          if (a.enabled === false) continue
          if (!a.email) continue
          await sendTo(a.email, a.name || undefined)
        }
      }
    }

    // For 80% threshold email (free users only — skip if they also crossed 100% in same call)
    if (crosses80 && isFreeUser && !crosses100) {
      const upgradeLink = upgradeCreditsLink
      const sendFreeTierEmail = async (email: string, name?: string) => {
        const prefs = await getEmailPreferences(email)
        if (prefs?.unsubscribeAll || prefs?.unsubscribeNotifications) return

        const html = await renderFreeTierUpgradeEmail({
          userName: name,
          percentUsed: Math.min(100, Math.round(params.percentAfter)),
          currentUsage: params.currentUsageAfter,
          limit: params.limit,
          upgradeLink,
        })

        await sendEmail({
          to: email,
          subject: getEmailSubject('free-tier-upgrade'),
          html,
          emailType: 'notifications',
        })

        logger.info('Free tier upgrade email sent', {
          email,
          percentUsed: Math.round(params.percentAfter),
          currentUsage: params.currentUsageAfter,
          limit: params.limit,
        })
      }

      // Free users are always individual scope (not organization)
      if (params.scope === 'user' && params.userId && params.userEmail) {
        const rows = await db
          .select({ enabled: settings.billingUsageNotificationsEnabled })
          .from(settings)
          .where(eq(settings.userId, params.userId))
          .limit(1)
        if (rows.length > 0 && rows[0].enabled === false) return
        await sendFreeTierEmail(params.userEmail, params.userName)
      }
    }

    // For 100% threshold email (free users only — credits exhausted)
    if (crosses100 && isFreeUser) {
      const upgradeLink = upgradeCreditsLink
      const sendExhaustedEmail = async (email: string, name?: string) => {
        const prefs = await getEmailPreferences(email)
        if (prefs?.unsubscribeAll || prefs?.unsubscribeNotifications) return

        const html = await renderCreditsExhaustedEmail({
          userName: name,
          limit: params.limit,
          upgradeLink,
        })

        await sendEmail({
          to: email,
          subject: getEmailSubject('free-tier-exhausted'),
          html,
          emailType: 'notifications',
        })

        logger.info('Free tier credits exhausted email sent', {
          email,
          currentUsage: params.currentUsageAfter,
          limit: params.limit,
        })
      }

      if (params.scope === 'user' && params.userId && params.userEmail) {
        const rows = await db
          .select({ enabled: settings.billingUsageNotificationsEnabled })
          .from(settings)
          .where(eq(settings.userId, params.userId))
          .limit(1)
        if (rows.length > 0 && rows[0].enabled === false) return
        await sendExhaustedEmail(params.userEmail, params.userName)
      }
    }
  } catch (error) {
    logger.error('Failed to send usage threshold email', {
      scope: params.scope,
      userId: params.userId,
      organizationId: params.organizationId,
      error,
    })
  }
}
