import "server-only";
import { randomBytes } from "crypto";
import type { FulfillmentStatus, PaymentKind, PaymentPlan, Prisma } from "@prisma/client";
import { db, type Tx } from "./db";
import { loadCart } from "./cart";
import { quote, type QuoteLine } from "./pricing";
import { balanceDueDate, batchState, depositPerUnit } from "./preorder";
import { fmtWindow, addHours } from "./dates";
import { getSettings } from "./settings";

export class CheckoutError extends Error {}

export const FULFILLMENT_FLOW: FulfillmentStatus[] = [
  "PENDING_CONFIRMATION", "CONFIRMED", "SOURCING", "ARRIVED", "PACKED", "SHIPPED", "DELIVERED",
];

export const FULFILLMENT_LABEL: Record<FulfillmentStatus, string> = {
  PENDING_CONFIRMATION: "Pending confirmation",
  CONFIRMED: "Confirmed",
  SOURCING: "Sourcing / in production",
  ARRIVED: "Arrived at studio",
  PACKED: "Packed",
  SHIPPED: "Shipped",
  DELIVERED: "Delivered",
  CANCELLED: "Cancelled",
};

export const PAYMENT_LABEL = {
  UNPAID: "Unpaid",
  PENDING_VERIFICATION: "Payment submitted, awaiting verification",
  PARTIALLY_PAID: "Partly paid",
  PAID: "Paid in full",
  PARTIALLY_REFUNDED: "Partly refunded",
  REFUNDED: "Refunded",
} as const;

// ───────────────────────────── shipping zones ─────────────────────────────

export async function zoneForDistrict(district: string) {
  const zones = await db.shippingZone.findMany({ where: { active: true }, orderBy: { sortOrder: "asc" } });
  const d = district.trim().toLowerCase();
  const match = zones.find((z) => (z.districts as string[]).some((x) => x.toLowerCase() === d));
  return match ?? zones.find((z) => (z.districts as string[]).includes("*")) ?? null;
}

import type { DiscountCode } from "@prisma/client";
export type DiscountResult = { error: string; code?: undefined } | { code: DiscountCode; error?: undefined } | null;

export async function findDiscount(code: string | undefined | null, now = new Date()): Promise<DiscountResult> {
  if (!code) return null;
  const dc = await db.discountCode.findUnique({ where: { code: code.trim().toUpperCase() } });
  if (!dc || !dc.active) return { error: "This discount code isn't valid." };
  if (dc.startsAt && dc.startsAt > now) return { error: "This discount code isn't active yet." };
  if (dc.endsAt && dc.endsAt < now) return { error: "This discount code has expired." };
  if (dc.usageLimit != null && dc.usedCount >= dc.usageLimit) return { error: "This discount code has been fully used." };
  return { code: dc };
}

// ───────────────────────────── quoting the current cart ─────────────────────────────

export async function quoteCart(opts: { district?: string; plan: PaymentPlan; splitShipment: boolean; discountCode?: string | null }) {
  const [cart, settings] = await Promise.all([loadCart(), getSettings()]);
  const zone = opts.district ? await zoneForDistrict(opts.district) : null;
  const dc = await findDiscount(opts.discountCode);
  const lines: QuoteLine[] = cart.lines.map((l) => ({
    key: l.variantId,
    unitPrice: l.unitPrice,
    qty: l.qty,
    mode: l.mode,
    batch: l.batch ? { ...l.batch, balanceDueAt: balanceDueDate(l.batch) } : null,
  }));
  const q = quote({
    lines,
    zone: zone ? { fee: zone.fee, freeOver: zone.freeOver } : null,
    discount: dc?.code ? { type: dc.code.type, value: dc.code.value, minSubtotal: dc.code.minSubtotal } : null,
    plan: opts.plan,
    splitShipment: opts.splitShipment && settings.preorder.splitShipmentAllowed,
    methods: { full: settings.payments.fullEnabled, deposit: settings.payments.depositEnabled, cod: settings.payments.codEnabled },
  });
  if (dc?.error) q.errors.push(dc.error);
  for (const l of cart.lines) if (l.problem) q.errors.push(`${l.name}: ${l.problem}`);
  return { cart, zone, quote: q, discount: dc?.code ?? null, settings };
}

// ───────────────────────────── capacity primitives ─────────────────────────────

/** Atomically reserve pre-order capacity. Returns false when the batch can't take `qty` more. */
async function holdBatch(tx: Tx, batchId: string, qty: number, now: Date) {
  const n = await tx.$executeRaw`
    UPDATE \`PreorderBatch\`
       SET \`heldQty\` = \`heldQty\` + ${qty}
     WHERE \`id\` = ${batchId}
       AND \`manuallyClosed\` = false
       AND \`opensAt\` <= ${now} AND \`closesAt\` >= ${now}
       AND \`heldQty\` + \`committedQty\` + ${qty} <= \`capacity\``;
  return n === 1;
}

async function takeStock(tx: Tx, variantId: string, qty: number) {
  const n = await tx.$executeRaw`
    UPDATE \`ProductVariant\` SET \`stockQty\` = \`stockQty\` - ${qty}
     WHERE \`id\` = ${variantId} AND \`stockQty\` >= ${qty}`;
  return n === 1;
}

type ItemForCapacity = { id: string; mode: "READY_STOCK" | "PRE_ORDER"; variantId: string | null; batchId: string | null; qty: number; capacityState: "HELD" | "COMMITTED" | "RELEASED" };

async function releaseItems(tx: Tx, items: ItemForCapacity[]) {
  for (const it of items) {
    if (it.capacityState === "RELEASED") continue;
    if (it.mode === "READY_STOCK" && it.variantId) {
      await tx.$executeRaw`UPDATE \`ProductVariant\` SET \`stockQty\` = \`stockQty\` + ${it.qty} WHERE \`id\` = ${it.variantId}`;
    } else if (it.mode === "PRE_ORDER" && it.batchId) {
      if (it.capacityState === "HELD")
        await tx.$executeRaw`UPDATE \`PreorderBatch\` SET \`heldQty\` = GREATEST(\`heldQty\` - ${it.qty}, 0) WHERE \`id\` = ${it.batchId}`;
      else
        await tx.$executeRaw`UPDATE \`PreorderBatch\` SET \`committedQty\` = GREATEST(\`committedQty\` - ${it.qty}, 0) WHERE \`id\` = ${it.batchId}`;
    }
    await tx.orderItem.update({ where: { id: it.id }, data: { capacityState: "RELEASED" } });
  }
}

async function commitItems(tx: Tx, items: ItemForCapacity[]) {
  for (const it of items) {
    if (it.capacityState !== "HELD") continue;
    if (it.mode === "PRE_ORDER" && it.batchId) {
      await tx.$executeRaw`
        UPDATE \`PreorderBatch\`
           SET \`heldQty\` = GREATEST(\`heldQty\` - ${it.qty}, 0), \`committedQty\` = \`committedQty\` + ${it.qty}
         WHERE \`id\` = ${it.batchId}`;
    }
    await tx.orderItem.update({ where: { id: it.id }, data: { capacityState: "COMMITTED" } });
  }
}

// ───────────────────────────── placing an order ─────────────────────────────

export type PlaceOrderInput = {
  customerName: string;
  phone: string;
  email: string;
  address: string;
  district: string;
  area: string;
  instructions?: string;
  plan: PaymentPlan;
  method: "BKASH" | "NAGAD" | "BANK" | "COD";
  splitShipment: boolean;
  discountCode?: string | null;
  acceptTerms: boolean;
  userId?: string | null;
};

function orderNumber() {
  const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
  const bytes = randomBytes(6);
  return "VL-" + Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
}

export async function placeOrder(input: PlaceOrderInput) {
  await expireHolds().catch(() => undefined);
  const { cart, zone, quote: q, discount, settings } = await quoteCart({
    district: input.district,
    plan: input.plan,
    splitShipment: input.splitShipment,
    discountCode: input.discountCode,
  });
  if (!cart.id || !cart.lines.length) throw new CheckoutError("Your bag is empty.");
  if (q.errors.length) throw new CheckoutError(q.errors[0]);
  if (!zone) throw new CheckoutError("We don't deliver to that district yet.");
  if (q.hasPreorder && !input.acceptTerms) throw new CheckoutError("Please accept the pre-order terms to continue.");
  if (input.plan === "COD" && input.method !== "COD") throw new CheckoutError("Choose cash on delivery as the payment method.");
  if (input.plan !== "COD") {
    const p = settings.payments;
    const ok = (input.method === "BKASH" && p.bkash.enabled) || (input.method === "NAGAD" && p.nagad.enabled) || (input.method === "BANK" && p.bank.enabled);
    if (!ok) throw new CheckoutError("That payment method isn't available.");
  }

  const now = new Date();
  const holdExpiresAt = input.plan === "COD" ? null : addHours(now, settings.payments.holdHours);

  const created = await db.$transaction(
    async (tx) => {
      // 1. Reserve stock / capacity atomically, line by line
      for (const l of cart.lines) {
        if (l.mode === "READY_STOCK") {
          if (!(await takeStock(tx, l.variantId, l.qty))) throw new CheckoutError(`${l.name} (${l.variantLabel || "one size"}) just sold out. Update your bag to continue.`);
        } else {
          if (!l.batch || batchState(l.batch, now) !== "OPEN") throw new CheckoutError(`Pre-orders for ${l.name} are closed.`);
          if (!(await holdBatch(tx, l.batch.id, l.qty, now))) throw new CheckoutError(`The ${l.name} batch doesn't have ${l.qty} place${l.qty > 1 ? "s" : ""} left. Reduce the quantity to continue.`);
        }
      }
      // 2. Discount usage (atomic, respects usage limit)
      if (discount) {
        const n = await tx.$executeRaw`
          UPDATE \`DiscountCode\` SET \`usedCount\` = \`usedCount\` + 1
           WHERE \`id\` = ${discount.id} AND (\`usageLimit\` IS NULL OR \`usedCount\` < \`usageLimit\`)`;
        if (n !== 1) throw new CheckoutError("This discount code has been fully used.");
      }
      // 3. Order with immutable item snapshots
      const items: Prisma.OrderItemCreateWithoutOrderInput[] = cart.lines.map((l) => ({
        productId: l.productId,
        variantId: l.variantId,
        categorySlug: l.categorySlug,
        productName: l.name,
        variantLabel: l.variantLabel,
        sku: l.sku,
        imageUrl: l.image,
        unitPrice: l.unitPrice,
        qty: l.qty,
        lineTotal: l.lineTotal,
        mode: l.mode,
        batchId: l.batch?.id ?? null,
        batchCode: l.batch?.code ?? null,
        dispatchWindow: l.batch ? fmtWindow(l.batch.dispatchFrom, l.batch.dispatchTo) : null,
        depositPerUnit: l.batch && input.plan === "DEPOSIT" ? depositPerUnit(l.batch, l.unitPrice) : 0,
        capacityState: "HELD",
      }));

      let number = orderNumber();
      for (let i = 0; i < 5 && (await tx.order.findUnique({ where: { number }, select: { id: true } })); i++) number = orderNumber();

      const order = await tx.order.create({
        data: {
          number,
          trackingToken: randomBytes(18).toString("base64url"),
          userId: input.userId ?? null,
          customerName: input.customerName,
          phone: input.phone,
          email: input.email.toLowerCase(),
          address: input.address,
          district: input.district,
          area: input.area,
          instructions: input.instructions || null,
          shippingZoneName: zone.name,
          subtotal: q.subtotal,
          discountTotal: q.discount,
          discountCode: discount?.code ?? null,
          shippingFee: q.shipping,
          total: q.total,
          dueNow: q.dueNow,
          paymentPlan: input.plan,
          paymentMethod: input.method,
          balanceDueAt: q.balanceDueAt,
          hasPreorder: q.hasPreorder,
          fulfillmentRule: q.mixed && input.splitShipment ? "SHIP_SEPARATELY" : "SHIP_TOGETHER",
          termsAcceptedAt: q.hasPreorder ? now : null,
          holdExpiresAt,
          items: { create: items },
          events: {
            create: [
              { type: "SYSTEM", message: `Order placed. ${input.plan === "COD" ? "Cash on delivery." : `Awaiting ${input.method} payment of ৳${q.dueNow.toLocaleString("en-IN")}.`}` },
              ...(q.hasPreorder ? [{ type: "SYSTEM", message: "Customer accepted the pre-order terms." }] : []),
            ],
          },
        },
      });
      await tx.cartItem.deleteMany({ where: { cartId: cart.id! } });
      return order;
    },
    { timeout: 20_000, isolationLevel: "ReadCommitted" },
  );
  return created;
}

// ───────────────────────────── payments ─────────────────────────────

function derivePaymentStatus(o: { amountPaid: number; amountRefunded: number; total: number }, hasSubmitted: boolean) {
  if (o.amountRefunded > 0 && o.amountPaid <= 0) return "REFUNDED" as const;
  if (o.amountRefunded > 0) return "PARTIALLY_REFUNDED" as const;
  if (o.amountPaid >= o.total) return "PAID" as const;
  if (o.amountPaid > 0) return "PARTIALLY_PAID" as const;
  return hasSubmitted ? ("PENDING_VERIFICATION" as const) : ("UNPAID" as const);
}

async function refreshPaymentState(tx: Tx, orderId: string) {
  const order = await tx.order.findUniqueOrThrow({ where: { id: orderId }, include: { items: true, payments: true } });
  const verified = order.payments.filter((p) => p.status === "VERIFIED");
  const received = verified.filter((p) => p.kind !== "REFUND").reduce((s, p) => s + p.amount, 0);
  const refunded = verified.filter((p) => p.kind === "REFUND").reduce((s, p) => s + p.amount, 0);
  const amountPaid = received - refunded;
  const hasSubmitted = order.payments.some((p) => p.status === "SUBMITTED");
  const paymentStatus = derivePaymentStatus({ amountPaid, amountRefunded: refunded, total: order.total }, hasSubmitted);
  const secured = order.fulfillmentStatus !== "CANCELLED" && order.dueNow > 0 && received >= order.dueNow;
  if (secured) await commitItems(tx, order.items);
  return tx.order.update({
    where: { id: orderId },
    data: { amountPaid, amountRefunded: refunded, paymentStatus, ...(secured ? { holdExpiresAt: null } : {}) },
  });
}

/** Customer submits a manual bKash / Nagad / bank transfer. Stays unverified until staff approve it. */
export async function submitPaymentProof(token: string, data: { amount: number; reference: string; senderNumber: string; method: string }) {
  const order = await db.order.findUnique({ where: { trackingToken: token }, include: { payments: true } });
  if (!order) throw new CheckoutError("Order not found.");
  if (order.fulfillmentStatus === "CANCELLED") throw new CheckoutError("This order was cancelled.");
  if (order.paymentPlan === "COD") throw new CheckoutError("This order is cash on delivery.");
  const outstanding = order.total - order.amountPaid - order.payments.filter((p) => p.status === "SUBMITTED").reduce((s, p) => s + p.amount, 0);
  if (data.amount <= 0 || data.amount > outstanding) throw new CheckoutError(`Enter an amount between ৳1 and ৳${Math.max(0, outstanding).toLocaleString("en-IN")}.`);
  const dup = await db.payment.findFirst({ where: { reference: data.reference, method: data.method, status: { not: "REJECTED" } } });
  if (dup) throw new CheckoutError("This transaction ID has already been submitted.");
  const kind: PaymentKind = order.amountPaid > 0 ? "BALANCE" : data.amount >= order.total ? "FULL" : "DEPOSIT";
  await db.$transaction(async (tx) => {
    await tx.payment.create({ data: { orderId: order.id, kind, method: data.method, amount: data.amount, reference: data.reference, senderNumber: data.senderNumber } });
    await tx.orderEvent.create({ data: { orderId: order.id, type: "PAYMENT", message: `Customer submitted a ${data.method} payment of ৳${data.amount.toLocaleString("en-IN")} (TrxID ${data.reference}). Awaiting verification.` } });
    await refreshPaymentState(tx, order.id);
  });
}

export async function reviewPayment(paymentId: string, approve: boolean, actorId: string, note?: string) {
  return db.$transaction(async (tx) => {
    const p = await tx.payment.findUniqueOrThrow({ where: { id: paymentId } });
    if (p.status !== "SUBMITTED") throw new CheckoutError("This payment has already been reviewed.");
    await tx.payment.update({ where: { id: p.id }, data: { status: approve ? "VERIFIED" : "REJECTED", verifiedById: actorId, verifiedAt: new Date(), note: note || p.note } });
    await tx.orderEvent.create({
      data: { orderId: p.orderId, type: "PAYMENT", actorId, message: approve ? `Payment of ৳${p.amount.toLocaleString("en-IN")} verified (${p.method} ${p.reference ?? ""}).` : `Payment of ৳${p.amount.toLocaleString("en-IN")} rejected${note ? `: ${note}` : "."}` },
    });
    return refreshPaymentState(tx, p.orderId);
  });
}

/** Staff-recorded receipt (e.g. cash on delivery collected, cash in studio). Verified immediately and idempotent per key. */
export async function recordReceipt(orderId: string, data: { kind: PaymentKind; method: string; amount: number; reference?: string; note?: string; idempotencyKey: string }, actorId: string) {
  if (data.amount <= 0) throw new CheckoutError("Amount must be greater than zero.");
  return db.$transaction(async (tx) => {
    const existing = await tx.payment.findUnique({ where: { idempotencyKey: data.idempotencyKey } });
    if (existing) return tx.order.findUniqueOrThrow({ where: { id: orderId } });
    const order = await tx.order.findUniqueOrThrow({ where: { id: orderId } });
    if (data.kind === "REFUND") {
      if (data.amount > order.amountPaid) throw new CheckoutError(`You can refund at most ৳${order.amountPaid.toLocaleString("en-IN")}.`);
    } else if (data.amount > order.total - order.amountPaid) {
      throw new CheckoutError(`Only ৳${(order.total - order.amountPaid).toLocaleString("en-IN")} is outstanding.`);
    }
    await tx.payment.create({ data: { orderId, kind: data.kind, method: data.method, amount: data.amount, reference: data.reference || null, note: data.note || null, status: "VERIFIED", verifiedById: actorId, verifiedAt: new Date(), idempotencyKey: data.idempotencyKey } });
    await tx.orderEvent.create({ data: { orderId, type: "PAYMENT", actorId, message: `${data.kind === "REFUND" ? "Refund" : "Payment"} of ৳${data.amount.toLocaleString("en-IN")} recorded (${data.method}).${data.note ? ` ${data.note}` : ""}` } });
    return refreshPaymentState(tx, orderId);
  });
}

// ───────────────────────────── fulfillment ─────────────────────────────

export async function updateFulfillment(orderId: string, to: FulfillmentStatus, actorId: string, extra?: { courier?: string; trackingNumber?: string; message?: string }) {
  return db.$transaction(async (tx) => {
    const o = await tx.order.findUniqueOrThrow({ where: { id: orderId }, include: { items: true } });
    if (o.fulfillmentStatus === "CANCELLED") throw new CheckoutError("Cancelled orders can't be updated.");
    if (to === "CANCELLED") throw new CheckoutError("Use Cancel order instead.");
    const from = FULFILLMENT_FLOW.indexOf(o.fulfillmentStatus);
    const target = FULFILLMENT_FLOW.indexOf(to);
    if (target <= from) throw new CheckoutError("Orders move forward only. Add a note if something needs correcting.");
    const received = o.amountPaid;
    if (target >= FULFILLMENT_FLOW.indexOf("CONFIRMED") && o.paymentPlan !== "COD" && received < o.dueNow)
      throw new CheckoutError(`Verify the ${o.paymentPlan === "DEPOSIT" ? "deposit" : "payment"} (৳${o.dueNow.toLocaleString("en-IN")}) before confirming.`);
    if (target >= FULFILLMENT_FLOW.indexOf("SHIPPED") && o.paymentPlan !== "COD" && received < o.total)
      throw new CheckoutError(`৳${(o.total - received).toLocaleString("en-IN")} balance is still outstanding. Record or verify it before shipping.`);
    if (to === "SHIPPED" && !(extra?.courier && extra?.trackingNumber) && !(o.courier && o.trackingNumber))
      throw new CheckoutError("Add the courier and tracking number to mark as shipped.");
    // COD orders reserve capacity until staff confirm by phone; confirmation secures it.
    if (target >= FULFILLMENT_FLOW.indexOf("CONFIRMED")) await commitItems(tx, o.items);
    await tx.orderEvent.create({ data: { orderId, type: "STATUS", actorId, message: `Status changed to ${FULFILLMENT_LABEL[to]}.${extra?.message ? ` ${extra.message}` : ""}` } });
    return tx.order.update({
      where: { id: orderId },
      data: { fulfillmentStatus: to, courier: extra?.courier || o.courier, trackingNumber: extra?.trackingNumber || o.trackingNumber, holdExpiresAt: null },
    });
  });
}

export async function cancelOrder(orderId: string, reason: string, actorId: string | null) {
  return db.$transaction(async (tx) => {
    const o = await tx.order.findUniqueOrThrow({ where: { id: orderId }, include: { items: true } });
    if (o.fulfillmentStatus === "CANCELLED") return o;
    if (["SHIPPED", "DELIVERED"].includes(o.fulfillmentStatus)) throw new CheckoutError("Shipped orders can't be cancelled. Record a return and refund instead.");
    await releaseItems(tx, o.items);
    if (o.discountCode) await tx.$executeRaw`UPDATE \`DiscountCode\` SET \`usedCount\` = GREATEST(\`usedCount\` - 1, 0) WHERE \`code\` = ${o.discountCode}`;
    await tx.payment.updateMany({ where: { orderId, status: "SUBMITTED" }, data: { status: "REJECTED", note: "Order cancelled before verification" } });
    await tx.orderEvent.create({ data: { orderId, type: "STATUS", actorId, message: `Order cancelled: ${reason}${o.amountPaid > 0 ? ` ৳${o.amountPaid.toLocaleString("en-IN")} was paid; record the refund when it is sent.` : ""}` } });
    return tx.order.update({ where: { id: orderId }, data: { fulfillmentStatus: "CANCELLED", cancelledAt: new Date(), cancelReason: reason, holdExpiresAt: null } });
  });
}

/** Releases capacity for unpaid orders whose hold window passed with no payment submitted. Safe to call often. */
export async function expireHolds(now = new Date()) {
  const stale = await db.order.findMany({
    where: { holdExpiresAt: { lt: now }, fulfillmentStatus: "PENDING_CONFIRMATION", amountPaid: 0, payments: { none: { status: { in: ["SUBMITTED", "VERIFIED"] } } } },
    select: { id: true },
    take: 100,
  });
  for (const o of stale) await cancelOrder(o.id, "Payment wasn't received within the hold window, so the reservation was released.", null);
  return stale.length;
}

/** Exposed for the integration tests only. */
export const __test = { holdBatch, takeStock, refreshPaymentState };
