import {
  BetTicketStatus,
  BonusStatus,
  CasinoRoundStatus,
  LedgerDirection,
  LedgerEntryType,
  Prisma,
  Role,
  UserBonusStatus,
  UserStatus,
  VipRewardKind,
  WalletAccountType
} from "@prisma/client";
import { prisma } from "@/server/db";
import { decimal, ensureWallet, postLedgerEntry, type Money } from "@/server/wallet/ledger";

type Tx = Prisma.TransactionClient;

const VIP_TIME_ZONE = "Europe/Zurich";
const MIN_CASHBACK_AMOUNT = decimal("0.01");

export const vipLevelRewards = [
  { key: "bronze", name: "Bronze", minVolume: "500", bonusAmount: "5" },
  { key: "silver", name: "Argent", minVolume: "5000", bonusAmount: "60" },
  { key: "gold", name: "Or", minVolume: "50000", bonusAmount: "250" },
  { key: "platinum", name: "Platine", minVolume: "150000", bonusAmount: "500" },
  { key: "jade", name: "Jade", minVolume: "300000", bonusAmount: "1000" },
  { key: "sapphire", name: "Saphir", minVolume: "1000000", bonusAmount: "2500" },
  { key: "black", name: "Rubis", minVolume: "5000000", bonusAmount: "10000" }
] as const;

const vipTierOrder = [{ key: "wood", minVolume: "0" }, ...vipLevelRewards] as const;

const cashbackRates = {
  wood: { dailyLossBps: 15, dailyWagerBps: 0, weeklyLossBps: 120, weeklyWagerBps: 0, monthlyLossBps: 250, monthlyWagerBps: 1 },
  bronze: { dailyLossBps: 25, dailyWagerBps: 0, weeklyLossBps: 180, weeklyWagerBps: 1, monthlyLossBps: 400, monthlyWagerBps: 2 },
  silver: { dailyLossBps: 60, dailyWagerBps: 1, weeklyLossBps: 300, weeklyWagerBps: 1, monthlyLossBps: 650, monthlyWagerBps: 3 },
  gold: { dailyLossBps: 100, dailyWagerBps: 1, weeklyLossBps: 450, weeklyWagerBps: 2, monthlyLossBps: 900, monthlyWagerBps: 5 },
  platinum: { dailyLossBps: 130, dailyWagerBps: 1, weeklyLossBps: 550, weeklyWagerBps: 3, monthlyLossBps: 1100, monthlyWagerBps: 7 },
  jade: { dailyLossBps: 160, dailyWagerBps: 2, weeklyLossBps: 650, weeklyWagerBps: 4, monthlyLossBps: 1300, monthlyWagerBps: 9 },
  sapphire: { dailyLossBps: 200, dailyWagerBps: 3, weeklyLossBps: 800, weeklyWagerBps: 5, monthlyLossBps: 1600, monthlyWagerBps: 12 },
  black: { dailyLossBps: 250, dailyWagerBps: 4, weeklyLossBps: 1000, weeklyWagerBps: 8, monthlyLossBps: 2000, monthlyWagerBps: 18 }
} as const;

export async function recordWagerInTx(
  tx: Tx,
  input: {
    userId: string;
    currency: string;
    stake: Money;
    bonusDebit?: Money;
    referenceType: string;
    referenceId?: string;
    occurredAt?: Date;
  }
) {
  const stake = decimal(input.stake).toDecimalPlaces(6);
  if (stake.lte(0)) return;

  const now = input.occurredAt ?? new Date();
  await ensureVipProgressRowInTx(tx, input.userId);
  const updatedProgress = await tx.userVipProgress.update({
    where: { userId: input.userId },
    data: {
      rollingVolume: { increment: stake },
      lifetimeVolume: { increment: stake }
    }
  });
  const previousLifetime = updatedProgress.lifetimeVolume.minus(stake).toDecimalPlaces(6);
  const nextLifetime = updatedProgress.lifetimeVolume.toDecimalPlaces(6);
  const nextTierKey = tierKeyForVolume(nextLifetime);

  if (updatedProgress.tierKey !== nextTierKey) {
    await tx.userVipProgress.update({
      where: { userId: input.userId },
      data: { tierKey: nextTierKey }
    });
  }

  await progressActiveBonusesInTx(tx, {
    userId: input.userId,
    currency: input.currency,
    stake,
    bonusDebit: input.bonusDebit ?? 0,
    referenceType: input.referenceType,
    referenceId: input.referenceId,
    now
  });

  await grantLevelUpRewardsInTx(tx, {
    userId: input.userId,
    currency: input.currency,
    previousLifetime,
    nextLifetime,
    now
  });
}

export async function ensureVipRewardsForUser(userId: string, currency = "EUR", now = new Date()) {
  return prisma.$transaction(async (tx) => {
    const progress = await syncHistoricalVipProgressInTx(tx, { userId, currency, now });
    await grantDueCashbackRewardsInTx(tx, { userId, currency, tierKey: progress.tierKey, now });
    return progress;
  });
}

export async function ensureVipRewardsForAllUsers(now = new Date()) {
  const users = await prisma.user.findMany({
    where: {
      status: UserStatus.ACTIVE,
      roles: { has: Role.USER }
    },
    select: {
      id: true,
      profile: { select: { currency: true } }
    }
  });

  const results = [];
  for (const user of users) {
    const progress = await ensureVipRewardsForUser(user.id, user.profile?.currency ?? "EUR", now);
    results.push({
      userId: user.id,
      tierKey: progress.tierKey,
      lifetimeVolume: progress.lifetimeVolume.toString()
    });
  }

  return results;
}

export async function creditPayoutByStakeSourceInTx(
  tx: Tx,
  input: {
    userId: string;
    walletId: string;
    currency: string;
    stake: Money;
    payout: Money;
    availableDebit: Money;
    bonusDebit: Money;
    type: LedgerEntryType;
    referenceType: string;
    referenceId: string;
    idempotencyKey?: string;
    reason: string;
    metadata?: Prisma.InputJsonValue;
  }
) {
  const stake = decimal(input.stake).toDecimalPlaces(6);
  const payout = decimal(input.payout).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
  const bonusDebit = decimal(input.bonusDebit).toDecimalPlaces(6);
  const entries: Awaited<ReturnType<typeof postLedgerEntry>>[] = [];

  if (payout.lte(0)) return entries;

  const activeBonusCount = await tx.userBonus.count({
    where: {
      userId: input.userId,
      status: UserBonusStatus.ACTIVE,
      wagerRequired: { gt: 0 }
    }
  });
  const shouldLockBonusPayout = bonusDebit.gt(0) && stake.gt(0) && activeBonusCount > 0;
  const rawBonusPayout = shouldLockBonusPayout ? payout.mul(bonusDebit).div(stake).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN) : decimal(0);
  const bonusPayout = rawBonusPayout.gt(payout) ? payout : rawBonusPayout;
  const availablePayout = payout.minus(bonusPayout).toDecimalPlaces(6);

  if (availablePayout.gt(0)) {
    entries.push(
      await postLedgerEntry(tx, {
        walletId: input.walletId,
        accountType: WalletAccountType.AVAILABLE,
        direction: LedgerDirection.CREDIT,
        type: input.type,
        amount: availablePayout,
        currency: input.currency,
        referenceType: input.referenceType,
        referenceId: input.referenceId,
        idempotencyKey: input.idempotencyKey ? `${input.idempotencyKey}:available` : undefined,
        reason: input.reason,
        metadata: {
          ...(jsonObject(input.metadata) ?? {}),
          payoutSplit: {
            available: availablePayout.toString(),
            bonus: bonusPayout.toString()
          }
        } as Prisma.InputJsonValue
      })
    );
  }

  if (bonusPayout.gt(0)) {
    entries.push(
      await postLedgerEntry(tx, {
        walletId: input.walletId,
        accountType: WalletAccountType.BONUS,
        direction: LedgerDirection.CREDIT,
        type: input.type,
        amount: bonusPayout,
        currency: input.currency,
        referenceType: input.referenceType,
        referenceId: input.referenceId,
        idempotencyKey: input.idempotencyKey ? `${input.idempotencyKey}:bonus` : undefined,
        reason: `${input.reason} (bonus non retirable)`,
        metadata: {
          ...(jsonObject(input.metadata) ?? {}),
          payoutSplit: {
            available: availablePayout.toString(),
            bonus: bonusPayout.toString()
          }
        } as Prisma.InputJsonValue
      })
    );
    await allocateBonusPayoutInTx(tx, { userId: input.userId, amount: bonusPayout });
  }

  return entries;
}

async function syncHistoricalVipProgressInTx(tx: Tx, input: { userId: string; currency: string; now: Date }) {
  await ensureVipProgressRowInTx(tx, input.userId);
  const current = await tx.userVipProgress.findUniqueOrThrow({ where: { userId: input.userId } });
  const historicalVolume = await getHistoricalWagerVolumeInTx(tx, input.userId);
  const previousLifetime = current.lifetimeVolume;
  const nextLifetime = historicalVolume.gt(previousLifetime) ? historicalVolume : previousLifetime;
  const nextTierKey = tierKeyForVolume(nextLifetime);
  let appliedHistoricalDelta = false;

  if (historicalVolume.gt(previousLifetime)) {
    const delta = historicalVolume.minus(previousLifetime).toDecimalPlaces(6);
    const updated = await tx.userVipProgress.updateMany({
      where: { userId: input.userId, lifetimeVolume: previousLifetime },
      data: {
        tierKey: nextTierKey,
        rollingVolume: { increment: delta },
        lifetimeVolume: historicalVolume
      }
    });

    appliedHistoricalDelta = updated.count === 1;
  } else if (current.tierKey !== nextTierKey) {
    await tx.userVipProgress.update({
      where: { userId: input.userId },
      data: { tierKey: nextTierKey }
    });
  }

  if (appliedHistoricalDelta) {
    const delta = historicalVolume.minus(previousLifetime);
    await progressActiveBonusesInTx(tx, {
      userId: input.userId,
      currency: input.currency,
      stake: delta,
      bonusDebit: 0,
      referenceType: "vip_backfill",
      referenceId: input.userId,
      now: input.now
    });
    await grantLevelUpRewardsInTx(tx, {
      userId: input.userId,
      currency: input.currency,
      previousLifetime,
      nextLifetime: historicalVolume,
      now: input.now
    });
  }

  return {
    tierKey: nextTierKey,
    lifetimeVolume: nextLifetime,
    rollingVolume: current.rollingVolume
  };
}

async function ensureVipProgressRowInTx(tx: Tx, userId: string) {
  await tx.userVipProgress.upsert({
    where: { userId },
    create: {
      userId,
      tierKey: "wood",
      rollingVolume: 0,
      lifetimeVolume: 0
    },
    update: {}
  });
}

async function progressActiveBonusesInTx(
  tx: Tx,
  input: {
    userId: string;
    currency: string;
    stake: Prisma.Decimal;
    bonusDebit: Money;
    referenceType: string;
    referenceId?: string;
    now: Date;
  }
) {
  const bonuses = await tx.userBonus.findMany({
    where: {
      userId: input.userId,
      status: UserBonusStatus.ACTIVE,
      wagerRequired: { gt: 0 }
    },
    orderBy: { createdAt: "asc" }
  });

  if (!bonuses.length) return;

  const remainingByBonus = await consumeBonusRemainingInTx(tx, {
    bonuses,
    bonusDebit: input.bonusDebit
  });

  for (const bonus of bonuses) {
    const previousProgress = bonus.wagerProgress;
    const nextProgress = previousProgress.plus(input.stake).toDecimalPlaces(6);
    const completed = nextProgress.gte(bonus.wagerRequired);
    const remainingAmount = remainingByBonus.get(bonus.id) ?? bonus.remainingAmount;

    await tx.userBonus.update({
      where: { id: bonus.id },
      data: {
        wagerProgress: completed ? bonus.wagerRequired : nextProgress,
        status: completed ? UserBonusStatus.WAGERING_COMPLETE : UserBonusStatus.ACTIVE,
        completedAt: completed ? input.now : undefined
      }
    });

    if (completed) {
      await releaseCompletedBonusInTx(tx, {
        userId: input.userId,
        currency: input.currency,
        userBonusId: bonus.id,
        amount: remainingAmount,
        referenceType: input.referenceType,
        referenceId: input.referenceId,
        now: input.now
      });
    }
  }
}

async function consumeBonusRemainingInTx(tx: Tx, input: { bonuses: Array<{ id: string; remainingAmount: Prisma.Decimal }>; bonusDebit: Money }) {
  let debitLeft = decimal(input.bonusDebit).toDecimalPlaces(6);
  const remainingByBonus = new Map<string, Prisma.Decimal>();

  for (const bonus of input.bonuses) {
    if (debitLeft.lte(0)) {
      remainingByBonus.set(bonus.id, bonus.remainingAmount);
      continue;
    }

    const consumed = debitLeft.gt(bonus.remainingAmount) ? bonus.remainingAmount : debitLeft;
    const remainingAmount = bonus.remainingAmount.minus(consumed).toDecimalPlaces(6);
    debitLeft = debitLeft.minus(consumed).toDecimalPlaces(6);
    remainingByBonus.set(bonus.id, remainingAmount);

    if (!remainingAmount.equals(bonus.remainingAmount)) {
      await tx.userBonus.update({
        where: { id: bonus.id },
        data: { remainingAmount }
      });
    }
  }

  return remainingByBonus;
}

async function allocateBonusPayoutInTx(tx: Tx, input: { userId: string; amount: Money }) {
  let payoutLeft = decimal(input.amount).toDecimalPlaces(6);
  if (payoutLeft.lte(0)) return;

  const bonuses = await tx.userBonus.findMany({
    where: {
      userId: input.userId,
      status: UserBonusStatus.ACTIVE,
      wagerRequired: { gt: 0 }
    },
    orderBy: { createdAt: "asc" }
  });

  for (const bonus of bonuses) {
    if (payoutLeft.lte(0)) break;
    await tx.userBonus.update({
      where: { id: bonus.id },
      data: { remainingAmount: { increment: payoutLeft } }
    });
    payoutLeft = decimal(0);
  }
}

async function releaseCompletedBonusInTx(
  tx: Tx,
  input: {
    userId: string;
    currency: string;
    userBonusId: string;
    amount: Money;
    referenceType: string;
    referenceId?: string;
    now: Date;
  }
) {
  const requested = decimal(input.amount).toDecimalPlaces(6);
  if (requested.lte(0)) {
    await tx.userBonus.update({ where: { id: input.userBonusId }, data: { remainingAmount: 0 } });
    return;
  }

  const wallet = await ensureWallet(input.userId, input.currency, tx);
  await tx.$queryRaw`SELECT id FROM "WalletAccount" WHERE "walletId" = CAST(${wallet.id} AS uuid) FOR UPDATE`;
  const bonusAccount = await tx.walletAccount.findUnique({
    where: { walletId_type: { walletId: wallet.id, type: WalletAccountType.BONUS } }
  });
  const availableToRelease = bonusAccount && bonusAccount.balance.lt(requested) ? bonusAccount.balance : requested;

  if (availableToRelease.gt(0)) {
    await postLedgerEntry(tx, {
      walletId: wallet.id,
      accountType: WalletAccountType.BONUS,
      direction: LedgerDirection.DEBIT,
      type: LedgerEntryType.BONUS_DEBIT,
      amount: availableToRelease,
      currency: input.currency,
      referenceType: "user_bonus",
      referenceId: input.userBonusId,
      idempotencyKey: `user-bonus:${input.userBonusId}:release:bonus`,
      reason: "Bonus wagering completed",
      metadata: {
        sourceReferenceType: input.referenceType,
        sourceReferenceId: input.referenceId,
        completedAt: input.now.toISOString()
      } as Prisma.InputJsonValue
    });
    await postLedgerEntry(tx, {
      walletId: wallet.id,
      accountType: WalletAccountType.AVAILABLE,
      direction: LedgerDirection.CREDIT,
      type: LedgerEntryType.BONUS_RELEASE,
      amount: availableToRelease,
      currency: input.currency,
      referenceType: "user_bonus",
      referenceId: input.userBonusId,
      idempotencyKey: `user-bonus:${input.userBonusId}:release:available`,
      reason: "Bonus released to withdrawable balance",
      metadata: {
        sourceReferenceType: input.referenceType,
        sourceReferenceId: input.referenceId,
        completedAt: input.now.toISOString()
      } as Prisma.InputJsonValue
    });
  }

  await tx.userBonus.update({
    where: { id: input.userBonusId },
    data: { remainingAmount: 0 }
  });
}

async function grantLevelUpRewardsInTx(
  tx: Tx,
  input: {
    userId: string;
    currency: string;
    previousLifetime: Prisma.Decimal;
    nextLifetime: Prisma.Decimal;
    now: Date;
  }
) {
  for (const level of vipLevelRewards) {
    const threshold = decimal(level.minVolume);
    if (input.previousLifetime.lt(threshold) && input.nextLifetime.gte(threshold)) {
      await grantVipBonusInTx(tx, {
        userId: input.userId,
        currency: input.currency,
        kind: VipRewardKind.LEVEL_UP,
        periodKey: level.key,
        tierKey: level.key,
        amount: level.bonusAmount,
        promotionCode: `VIP_LEVEL_${level.key.toUpperCase()}`,
        promotionName: `Bonus VIP ${level.name}`,
        terms: `Bonus de montée ${level.name}, wager x1 avant retrait.`,
        now: input.now
      });
    }
  }
}

async function grantDueCashbackRewardsInTx(tx: Tx, input: { userId: string; currency: string; tierKey: string; now: Date }) {
  const periods = [dailyPeriod(input.now), weeklyPeriod(input.now), monthlyPeriod(input.now)];

  for (const period of periods) {
    const stats = await getPeriodStatsInTx(tx, {
      userId: input.userId,
      start: period.start,
      end: period.end
    });
    const amount = calculateCashbackAmount(period.kind, input.tierKey, stats.wager, stats.loss);
    if (amount.lt(MIN_CASHBACK_AMOUNT)) continue;

    await grantVipBonusInTx(tx, {
      userId: input.userId,
      currency: input.currency,
      kind: period.kind,
      periodKey: period.periodKey,
      tierKey: input.tierKey,
      amount,
      promotionCode: promotionCodeForCashback(period.kind),
      promotionName: period.name,
      terms: `${period.name} crédité automatiquement, wager x1 avant retrait.`,
      now: input.now
    });
  }
}

async function grantVipBonusInTx(
  tx: Tx,
  input: {
    userId: string;
    currency: string;
    kind: VipRewardKind;
    periodKey: string;
    tierKey?: string;
    amount: Money;
    promotionCode: string;
    promotionName: string;
    terms: string;
    now: Date;
  }
) {
  const amount = decimal(input.amount).toDecimalPlaces(6);
  if (amount.lt(MIN_CASHBACK_AMOUNT)) return;

  const existingGrant = await tx.vipRewardGrant.findUnique({
    where: {
      userId_kind_periodKey: {
        userId: input.userId,
        kind: input.kind,
        periodKey: input.periodKey
      }
    }
  });
  if (existingGrant) return;

  const promotion = await tx.bonusPromotion.upsert({
    where: { code: input.promotionCode },
    create: {
      code: input.promotionCode,
      name: input.promotionName,
      status: BonusStatus.ACTIVE,
      type: promotionTypeForReward(input.kind),
      currency: input.currency,
      wageringMultiplier: "1",
      contributionRules: {
        vip: true,
        kind: input.kind,
        wagerMultiplier: 1
      },
      startsAt: input.now,
      terms: input.terms
    },
    update: {
      name: input.promotionName,
      status: BonusStatus.ACTIVE,
      type: promotionTypeForReward(input.kind),
      currency: input.currency,
      wageringMultiplier: "1",
      contributionRules: {
        vip: true,
        kind: input.kind,
        wagerMultiplier: 1
      },
      terms: input.terms
    }
  });

  const userBonus = await tx.userBonus.create({
    data: {
      userId: input.userId,
      promotionId: promotion.id,
      status: UserBonusStatus.OFFERED,
      bonusAmount: amount,
      wagerRequired: amount,
      wagerProgress: 0,
      remainingAmount: amount
    }
  });

  await tx.vipRewardGrant.create({
    data: {
      userId: input.userId,
      kind: input.kind,
      tierKey: input.tierKey,
      periodKey: input.periodKey,
      amount,
      wagerRequired: amount,
      userBonusId: userBonus.id
    }
  });
}

async function getHistoricalWagerVolumeInTx(tx: Tx, userId: string) {
  const [casino, sports] = await Promise.all([
    tx.casinoRound.aggregate({
      where: {
        userId,
        status: { in: [CasinoRoundStatus.ACTIVE, CasinoRoundStatus.WON, CasinoRoundStatus.LOST] }
      },
      _sum: { stake: true }
    }),
    tx.betTicket.aggregate({
      where: {
        userId,
        status: { notIn: [BetTicketStatus.VOID, BetTicketStatus.CANCELLED] }
      },
      _sum: { stake: true }
    })
  ]);

  return decimal(casino._sum.stake ?? 0).plus(sports._sum.stake ?? 0).toDecimalPlaces(6);
}

async function getPeriodStatsInTx(tx: Tx, input: { userId: string; start: Date; end: Date }) {
  const [casino, sportsWager, sportsLoss] = await Promise.all([
    tx.casinoRound.aggregate({
      where: {
        userId: input.userId,
        status: { in: [CasinoRoundStatus.WON, CasinoRoundStatus.LOST] },
        createdAt: { gte: input.start, lt: input.end }
      },
      _sum: { stake: true, profit: true }
    }),
    tx.betTicket.aggregate({
      where: {
        userId: input.userId,
        status: { notIn: [BetTicketStatus.VOID, BetTicketStatus.CANCELLED] },
        placedAt: { gte: input.start, lt: input.end }
      },
      _sum: { stake: true }
    }),
    tx.betTicket.aggregate({
      where: {
        userId: input.userId,
        status: BetTicketStatus.LOST,
        settledAt: { gte: input.start, lt: input.end }
      },
      _sum: { stake: true }
    })
  ]);

  const casinoWager = decimal(casino._sum.stake ?? 0);
  const casinoProfit = decimal(casino._sum.profit ?? 0);
  const casinoLoss = casinoProfit.lt(0) ? casinoProfit.abs() : decimal(0);

  return {
    wager: casinoWager.plus(sportsWager._sum.stake ?? 0).toDecimalPlaces(6),
    loss: casinoLoss.plus(sportsLoss._sum.stake ?? 0).toDecimalPlaces(6)
  };
}

function calculateCashbackAmount(kind: VipRewardKind, tierKey: string, wager: Prisma.Decimal, loss: Prisma.Decimal) {
  const rates = cashbackRates[(tierKey in cashbackRates ? tierKey : "wood") as keyof typeof cashbackRates];
  const lossBps =
    kind === VipRewardKind.DAILY_CASHBACK ? rates.dailyLossBps : kind === VipRewardKind.WEEKLY_CASHBACK ? rates.weeklyLossBps : rates.monthlyLossBps;
  const wagerBps =
    kind === VipRewardKind.DAILY_CASHBACK ? rates.dailyWagerBps : kind === VipRewardKind.WEEKLY_CASHBACK ? rates.weeklyWagerBps : rates.monthlyWagerBps;

  return loss.mul(lossBps).div(10000).plus(wager.mul(wagerBps).div(10000)).toDecimalPlaces(2, Prisma.Decimal.ROUND_DOWN);
}

function tierKeyForVolume(volume: Prisma.Decimal) {
  return vipTierOrder.reduce((current, tier) => (volume.gte(decimal(tier.minVolume)) ? tier.key : current), "wood");
}

function promotionCodeForCashback(kind: VipRewardKind) {
  if (kind === VipRewardKind.DAILY_CASHBACK) return "VIP_DAILY_CASHBACK";
  if (kind === VipRewardKind.WEEKLY_CASHBACK) return "VIP_WEEKLY_CASHBACK";
  return "VIP_MONTHLY_CASHBACK";
}

function promotionTypeForReward(kind: VipRewardKind) {
  if (kind === VipRewardKind.LEVEL_UP) return "vip_level_up";
  if (kind === VipRewardKind.DAILY_CASHBACK) return "vip_daily_cashback";
  if (kind === VipRewardKind.WEEKLY_CASHBACK) return "vip_weekly_cashback";
  return "vip_monthly_cashback";
}

function jsonObject(value: Prisma.InputJsonValue | undefined) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
  return value as Record<string, Prisma.InputJsonValue>;
}

function dailyPeriod(now: Date) {
  const today = localDateKey(now);
  const date = shiftLocalDate(today, -1);
  return {
    kind: VipRewardKind.DAILY_CASHBACK,
    name: "Cashback quotidien",
    periodKey: `daily:${date}`,
    start: zonedDateTimeToUtc(parseLocalDate(date)),
    end: zonedDateTimeToUtc(parseLocalDate(shiftLocalDate(date, 1)))
  };
}

function weeklyPeriod(now: Date) {
  const parts = zonedParts(now);
  const today = localDateKey(now);
  const daysSinceThursday = (parts.weekday - 4 + 7) % 7;
  let triggerDate = shiftLocalDate(today, -daysSinceThursday);
  let trigger = zonedDateTimeToUtc({ ...parseLocalDate(triggerDate), hour: 12 });

  if (now < trigger) {
    triggerDate = shiftLocalDate(triggerDate, -7);
    trigger = zonedDateTimeToUtc({ ...parseLocalDate(triggerDate), hour: 12 });
  }

  const startDate = shiftLocalDate(triggerDate, -7);
  return {
    kind: VipRewardKind.WEEKLY_CASHBACK,
    name: "Cashback hebdomadaire",
    periodKey: `weekly:${startDate}:${triggerDate}`,
    start: zonedDateTimeToUtc({ ...parseLocalDate(startDate), hour: 12 }),
    end: trigger
  };
}

function monthlyPeriod(now: Date) {
  const parts = zonedParts(now);
  const previousMonth = parts.month === 1 ? 12 : parts.month - 1;
  const previousYear = parts.month === 1 ? parts.year - 1 : parts.year;
  const previousKey = `${previousYear}-${String(previousMonth).padStart(2, "0")}`;
  const currentMonthStart = zonedDateTimeToUtc({ year: parts.year, month: parts.month, day: 1 });

  return {
    kind: VipRewardKind.MONTHLY_CASHBACK,
    name: "Cashback mensuel",
    periodKey: `monthly:${previousKey}`,
    start: zonedDateTimeToUtc({ year: previousYear, month: previousMonth, day: 1 }),
    end: currentMonthStart
  };
}

function zonedParts(date: Date) {
  const parts = new Intl.DateTimeFormat("en-US", {
    timeZone: VIP_TIME_ZONE,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    weekday: "short",
    hourCycle: "h23"
  }).formatToParts(date);
  const map = Object.fromEntries(parts.map((part) => [part.type, part.value]));
  const weekdays: Record<string, number> = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };

  return {
    year: Number(map.year),
    month: Number(map.month),
    day: Number(map.day),
    hour: Number(map.hour),
    minute: Number(map.minute),
    second: Number(map.second),
    weekday: weekdays[map.weekday] ?? 1
  };
}

function localDateKey(date: Date) {
  const parts = zonedParts(date);
  return `${parts.year}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
}

function parseLocalDate(value: string) {
  const [year, month, day] = value.split("-").map(Number);
  return { year, month, day };
}

function shiftLocalDate(dateKey: string, days: number) {
  const { year, month, day } = parseLocalDate(dateKey);
  const shifted = new Date(Date.UTC(year, month - 1, day + days, 12));
  return `${shifted.getUTCFullYear()}-${String(shifted.getUTCMonth() + 1).padStart(2, "0")}-${String(shifted.getUTCDate()).padStart(2, "0")}`;
}

function zonedDateTimeToUtc(input: { year: number; month: number; day: number; hour?: number; minute?: number; second?: number }) {
  const hour = input.hour ?? 0;
  const minute = input.minute ?? 0;
  const second = input.second ?? 0;
  const guess = new Date(Date.UTC(input.year, input.month - 1, input.day, hour, minute, second));
  const observed = zonedParts(guess);
  const targetUtc = Date.UTC(input.year, input.month - 1, input.day, hour, minute, second);
  const observedUtc = Date.UTC(observed.year, observed.month - 1, observed.day, observed.hour, observed.minute, observed.second);
  return new Date(guess.getTime() - (observedUtc - targetUtc));
}
