import { Plan } from "@prisma/client";
import {
  EXTRA_CREDITS_MAX,
  FREE_TOP_UP_CREDITS,
  MAX_CREDIT_CAP,
  monthlyCreditsForPlan,
} from "./credits";
import { prisma } from "./db";
import { getNextMonthlyQuotaReset } from "./plans";

export class CreditInsufficientError extends Error {
  readonly status = 402;
  constructor(message = "Payment Required: no credits remaining.") {
    super(message);
    this.name = "CreditInsufficientError";
  }
}

export type DeductResult = {
  fromMonthly: number;
  fromExtra: number;
};

export type RenewalBalances = {
  monthlyCredits: number;
  videosUsed: number;
  extraCredits: number;
  bonusQuota: number;
  trimmed: number;
  totalAfter: number;
};

/**
 * Rollover + hard cap on renewal.
 * prospective = unused (monthly remaining + extras) + newMonthly
 * If prospective > MAX_CREDIT_CAP (30), trim excess (no refund).
 *
 * Storage: prefer filling the new monthly allocation first, leftover as extras.
 */
export function computeRenewalBalances(input: {
  monthlyCredits: number;
  videosUsed: number;
  bonusQuota?: number;
  extraCredits: number;
  newMonthlyCredits: number;
  maxCap?: number;
}): RenewalBalances {
  const cap = input.maxCap ?? MAX_CREDIT_CAP;
  const bonus = input.bonusQuota ?? 0;
  const unusedMonthly = Math.max(0, input.monthlyCredits + bonus - input.videosUsed);
  const currentUnused = unusedMonthly + Math.max(0, input.extraCredits);
  const prospective = currentUnused + input.newMonthlyCredits;
  const totalAfter = Math.min(prospective, cap);
  const trimmed = Math.max(0, prospective - totalAfter);

  const monthlyCredits = Math.min(input.newMonthlyCredits, totalAfter);
  const extraCredits = Math.max(0, totalAfter - monthlyCredits);

  return {
    monthlyCredits,
    videosUsed: 0,
    extraCredits,
    bonusQuota: 0,
    trimmed,
    totalAfter,
  };
}

/**
 * Apply monthly credit renewal with MAX_CREDIT_CAP rollover trim.
 */
export async function applyMonthlyCreditRenewal(
  userId: string,
  opts: {
    plan?: Plan;
    newMonthlyCredits?: number;
    quotaResetAt?: Date;
  } = {},
) {
  return prisma.$transaction(async (tx) => {
    const user = await tx.user.findUniqueOrThrow({ where: { id: userId } });
    const plan = opts.plan ?? user.plan;
    const newMonthly = opts.newMonthlyCredits ?? monthlyCreditsForPlan(plan);
    const balances = computeRenewalBalances({
      monthlyCredits: user.monthlyCredits,
      videosUsed: user.videosUsed,
      bonusQuota: user.bonusQuota,
      extraCredits: user.extraCredits,
      newMonthlyCredits: newMonthly,
    });

    return tx.user.update({
      where: { id: userId },
      data: {
        plan,
        monthlyCredits: balances.monthlyCredits,
        videosUsed: balances.videosUsed,
        extraCredits: balances.extraCredits,
        bonusQuota: balances.bonusQuota,
        quotaResetAt: opts.quotaResetAt ?? getNextMonthlyQuotaReset(),
      },
    });
  });
}

/**
 * Deduct video creation credits for `count` items.
 * Order: monthly allocation first, then extraCredits.
 */
export async function deductUserCredit(
  userId: string,
  count = 1,
): Promise<DeductResult> {
  if (count <= 0) return { fromMonthly: 0, fromExtra: 0 };

  return prisma.$transaction(async (tx) => {
    const rows = await tx.$queryRaw<
      Array<{
        id: string;
        plan: Plan;
        videosUsed: number;
        monthlyCredits: number;
        bonusQuota: number;
        videoCredits: number;
      }>
    >`SELECT id, plan, "videosUsed", "monthlyCredits", "bonusQuota", "videoCredits"
      FROM "User" WHERE id = ${userId} FOR UPDATE`;

    const user = rows[0];
    if (!user) throw new Error("User not found");

    const monthlyLimit = user.monthlyCredits + user.bonusQuota;
    const monthlyRemaining = Math.max(0, monthlyLimit - user.videosUsed);
    const extra = user.videoCredits;
    const total = monthlyRemaining + extra;

    if (total < count) {
      throw new CreditInsufficientError(
        `Not enough credits. Available: ${total} (monthly remaining ${monthlyRemaining} + extras ${extra}).`,
      );
    }

    const fromMonthly = Math.min(count, monthlyRemaining);
    const fromExtra = count - fromMonthly;

    await tx.user.update({
      where: { id: userId },
      data: {
        ...(fromMonthly > 0 ? { videosUsed: { increment: fromMonthly } } : {}),
        ...(fromExtra > 0 ? { extraCredits: { decrement: fromExtra } } : {}),
      },
    });

    return { fromMonthly, fromExtra };
  });
}

/** Refund reserved credits (job create failure / worker failure). */
export async function refundUserCredit(
  userId: string,
  fromMonthly: number,
  fromExtra: number,
) {
  if (fromMonthly > 0) {
    await prisma.$executeRaw`
      UPDATE "User"
      SET "videosUsed" = GREATEST(0, "videosUsed" - ${fromMonthly})
      WHERE id = ${userId}
    `;
  }
  if (fromExtra > 0) {
    await prisma.$executeRaw`
      UPDATE "User"
      SET "videoCredits" = LEAST(${EXTRA_CREDITS_MAX}, "videoCredits" + ${fromExtra})
      WHERE id = ${userId}
    `;
  }
}

/**
 * Admin / support: reset or adjust a user's billing cycle and extras.
 */
export async function adminResetUserCredits(
  userId: string,
  options: {
    plan?: Plan;
    monthlyCredits?: number;
    creditsUsed?: number;
    extraCredits?: number;
    clearFreeTopUp?: boolean;
  } = {},
) {
  const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
  const plan = options.plan ?? user.plan;
  const monthlyCredits =
    options.monthlyCredits ?? monthlyCreditsForPlan(plan);

  const extras =
    options.extraCredits !== undefined
      ? Math.max(0, Math.min(EXTRA_CREDITS_MAX, options.extraCredits))
      : user.extraCredits;
  const used = options.creditsUsed ?? 0;
  const remainingMonthly = Math.max(0, monthlyCredits - used);
  const total = remainingMonthly + extras;
  const cappedExtras =
    total > MAX_CREDIT_CAP
      ? Math.max(0, MAX_CREDIT_CAP - remainingMonthly)
      : extras;

  return prisma.user.update({
    where: { id: userId },
    data: {
      plan,
      subscriptionStatus: plan === "PREMIUM" ? "pro" : "free",
      monthlyCredits,
      videosUsed: used,
      extraCredits: cappedExtras,
      ...(options.clearFreeTopUp ? { freeTopUpPurchased: false } : {}),
      quotaResetAt: getNextMonthlyQuotaReset(),
      ...(plan === "FREE"
        ? {
            stripeSubscriptionId: null,
            subscribedAt: null,
            cardLast4: null,
            bonusQuota: 0,
          }
        : {}),
    },
  });
}

export async function activateProPlan(
  userId: string,
  opts: {
    stripeCustomerId?: string | null;
    stripeSubscriptionId?: string | null;
    cardLast4?: string | null;
  } = {},
) {
  await applyMonthlyCreditRenewal(userId, {
    plan: "PREMIUM",
    newMonthlyCredits: monthlyCreditsForPlan("PREMIUM"),
  });

  return prisma.user.update({
    where: { id: userId },
    data: {
      plan: "PREMIUM",
      subscriptionStatus: "pro",
      subscribedAt: new Date(),
      ...(opts.stripeCustomerId !== undefined
        ? { stripeCustomerId: opts.stripeCustomerId }
        : {}),
      ...(opts.stripeSubscriptionId !== undefined
        ? { stripeSubscriptionId: opts.stripeSubscriptionId }
        : {}),
      ...(opts.cardLast4 !== undefined ? { cardLast4: opts.cardLast4 } : {}),
    },
  });
}

export async function downgradeToFreePlan(userId: string) {
  await applyMonthlyCreditRenewal(userId, {
    plan: "FREE",
    newMonthlyCredits: monthlyCreditsForPlan("FREE"),
  });

  return prisma.user.update({
    where: { id: userId },
    data: {
      plan: "FREE",
      subscriptionStatus: "free",
      stripeSubscriptionId: null,
      subscribedAt: null,
      cardLast4: null,
      apiKeyHash: null,
      apiKeyPrefix: null,
      apiRateLimitBonus: 0,
    },
  });
}

export async function grantExtraCredits(userId: string, credits: number) {
  if (credits <= 0) return;
  await prisma.$transaction(async (tx) => {
    const user = await tx.user.findUniqueOrThrow({ where: { id: userId } });
    const monthlyRemaining = Math.max(
      0,
      user.monthlyCredits + user.bonusQuota - user.videosUsed,
    );
    const room = Math.max(0, MAX_CREDIT_CAP - monthlyRemaining - user.extraCredits);
    const grant = Math.min(credits, room);
    if (grant <= 0) return;
    await tx.user.update({
      where: { id: userId },
      data: { extraCredits: { increment: grant } },
    });
  });
}

export async function markFreeTopUpPurchased(userId: string) {
  await prisma.user.update({
    where: { id: userId },
    data: { freeTopUpPurchased: true },
  });
  await grantExtraCredits(userId, FREE_TOP_UP_CREDITS);
}
